From c4fdc799366f80555c2d5e75efb6077e669047e8 Mon Sep 17 00:00:00 2001 From: Pavel Minaev Date: Thu, 28 Mar 2024 17:01:13 -0700 Subject: [PATCH] Variable paging --- src/debugpy/adapter/__init__.py | 61 +++++++++++++++++++- src/debugpy/adapter/clients.py | 51 ++-------------- src/debugpy/common/messaging.py | 2 +- src/debugpy/server/adapters.py | 80 ++++++++------------------ src/debugpy/server/eval.py | 28 ++++++--- src/debugpy/server/inspect/__init__.py | 16 ++++-- src/debugpy/server/inspect/stdlib.py | 22 +++++-- src/debugpy/server/tracing/__init__.py | 14 +++++ 8 files changed, 153 insertions(+), 121 deletions(-) diff --git a/src/debugpy/adapter/__init__.py b/src/debugpy/adapter/__init__.py index fa55b259..348d33ce 100644 --- a/src/debugpy/adapter/__init__.py +++ b/src/debugpy/adapter/__init__.py @@ -8,7 +8,66 @@ import typing if typing.TYPE_CHECKING: __all__: list[str] -__all__ = [] +__all__ = ["CAPABILITIES", "access_token"] + +EXCEPTION_FILTERS = [ + { + "filter": "raised", + "label": "Raised Exceptions", + "default": False, + "description": "Break whenever any exception is raised.", + }, + { + "filter": "uncaught", + "label": "Uncaught Exceptions", + "default": True, + "description": "Break when the process is exiting due to unhandled exception.", + }, + { + "filter": "userUnhandled", + "label": "User Uncaught Exceptions", + "default": False, + "description": "Break when exception escapes into library code.", + }, +] + +CAPABILITIES_V1 = { + "supportsCompletionsRequest": True, + "supportsConditionalBreakpoints": True, + "supportsConfigurationDoneRequest": True, + "supportsDebuggerProperties": True, + "supportsDelayedStackTraceLoading": True, + "supportsEvaluateForHovers": True, + "supportsExceptionInfoRequest": True, + "supportsExceptionOptions": True, + "supportsFunctionBreakpoints": True, + "supportsHitConditionalBreakpoints": True, + "supportsLogPoints": True, + "supportsModulesRequest": True, + "supportsSetExpression": True, + "supportsSetVariable": True, + "supportsValueFormattingOptions": True, + "supportsTerminateRequest": True, + "supportsGotoTargetsRequest": True, + "supportsClipboardContext": True, + "exceptionBreakpointFilters": EXCEPTION_FILTERS, + "supportsStepInTargetsRequest": True, +} + +CAPABILITIES_V2 = { + "supportsConfigurationDoneRequest": True, + "supportsConditionalBreakpoints": True, + "supportsHitConditionalBreakpoints": True, + "supportsEvaluateForHovers": True, + "exceptionBreakpointFilters": EXCEPTION_FILTERS, + "supportsSetVariable": True, + "supportsExceptionInfoRequest": True, + "supportsDelayedStackTraceLoading": True, + "supportsLogPoints": True, + "supportsSetExpression": True, + "supportsTerminateRequest": True, + "supportsClipboardContext": True, +} access_token = None """Access token used to authenticate with this adapter.""" diff --git a/src/debugpy/adapter/clients.py b/src/debugpy/adapter/clients.py index ee1d1514..f9ac3586 100644 --- a/src/debugpy/adapter/clients.py +++ b/src/debugpy/adapter/clients.py @@ -25,10 +25,14 @@ class Client(components.Component): class Capabilities(components.Capabilities): PROPERTIES = { + # defaults for optional properties in DAP "initialize request" "supportsVariableType": False, "supportsVariablePaging": False, "supportsRunInTerminalRequest": False, "supportsMemoryReferences": False, + "supportsProgressReporting": False, + "supportsInvalidatedEvent": False, + "supportsMemoryEvent": False, "supportsArgsCanBeInterpretedByShell": False, "supportsStartDebuggingRequest": False, } @@ -148,55 +152,12 @@ class Client(components.Component): def initialize_request(self, request): if self._initialize_request is not None: raise request.isnt_valid("Session is already initialized") - + self.client_id = request("clientID", "") self.capabilities = self.Capabilities(self, request) self.expectations = self.Expectations(self, request) self._initialize_request = request - - exception_breakpoint_filters = [ - { - "filter": "raised", - "label": "Raised Exceptions", - "default": False, - "description": "Break whenever any exception is raised.", - }, - { - "filter": "uncaught", - "label": "Uncaught Exceptions", - "default": True, - "description": "Break when the process is exiting due to unhandled exception.", - }, - { - "filter": "userUnhandled", - "label": "User Uncaught Exceptions", - "default": False, - "description": "Break when exception escapes into library code.", - }, - ] - - return { - "supportsCompletionsRequest": True, - "supportsConditionalBreakpoints": True, - "supportsConfigurationDoneRequest": True, - "supportsDebuggerProperties": True, - "supportsDelayedStackTraceLoading": True, - "supportsEvaluateForHovers": True, - "supportsExceptionInfoRequest": True, - "supportsExceptionOptions": True, - "supportsFunctionBreakpoints": True, - "supportsHitConditionalBreakpoints": True, - "supportsLogPoints": True, - "supportsModulesRequest": True, - "supportsSetExpression": True, - "supportsSetVariable": True, - "supportsValueFormattingOptions": True, - "supportsTerminateRequest": True, - "supportsGotoTargetsRequest": True, - "supportsClipboardContext": True, - "exceptionBreakpointFilters": exception_breakpoint_filters, - "supportsStepInTargetsRequest": True, - } + return adapter.CAPABILITIES_V2 # Common code for "launch" and "attach" request handlers. # diff --git a/src/debugpy/common/messaging.py b/src/debugpy/common/messaging.py index ce597348..07cdbdb0 100644 --- a/src/debugpy/common/messaging.py +++ b/src/debugpy/common/messaging.py @@ -372,7 +372,7 @@ class MessageDict(collections.OrderedDict): See debugpy.common.json for reusable validators. """ - if not validate: + if validate is None: validate = lambda x: x elif isinstance(validate, type) or isinstance(validate, tuple): validate = json.of_type(validate, optional=optional) diff --git a/src/debugpy/server/adapters.py b/src/debugpy/server/adapters.py index 8fd0764a..136a6e85 100644 --- a/src/debugpy/server/adapters.py +++ b/src/debugpy/server/adapters.py @@ -7,6 +7,7 @@ import sys import threading from itertools import islice +from debugpy import adapter from debugpy.adapter import components from debugpy.common import json, log, messaging, sockets from debugpy.common.messaging import MessageDict, Request @@ -28,14 +29,7 @@ class Adapter: """Represents the debug adapter connected to this debug server.""" class Capabilities(components.Capabilities): - PROPERTIES = { - "supportsVariableType": False, - "supportsVariablePaging": False, - "supportsRunInTerminalRequest": False, - "supportsMemoryReferences": False, - "supportsArgsCanBeInterpretedByShell": False, - "supportsStartDebuggingRequest": False, - } + PROPERTIES = {} class Expectations(components.Capabilities): PROPERTIES = { @@ -126,52 +120,7 @@ class Adapter: self._capabilities = self.Capabilities(None, request) self._expectations = self.Expectations(None, request) self._is_initialized = True - - exception_breakpoint_filters = [ - { - "filter": "raised", - "label": "Raised Exceptions", - "default": False, - "description": "Break whenever any exception is raised.", - }, - # TODO: https://github.com/microsoft/debugpy/issues/1453 - { - "filter": "uncaught", - "label": "Uncaught Exceptions", - "default": True, - "description": "Break when the process is exiting due to unhandled exception.", - }, - # TODO: https://github.com/microsoft/debugpy/issues/1454 - { - "filter": "userUncaught", - "label": "User Uncaught Exceptions", - "default": False, - "description": "Break when exception escapes into library code.", - }, - ] - - return { - "exceptionBreakpointFilters": exception_breakpoint_filters, - "supportsClipboardContext": True, - "supportsCompletionsRequest": True, - "supportsConditionalBreakpoints": True, - "supportsConfigurationDoneRequest": True, - "supportsDebuggerProperties": True, - "supportsDelayedStackTraceLoading": True, - "supportsEvaluateForHovers": True, - "supportsExceptionInfoRequest": True, - "supportsExceptionOptions": True, - "supportsFunctionBreakpoints": True, - "supportsGotoTargetsRequest": True, - "supportsHitConditionalBreakpoints": True, - "supportsLogPoints": True, - "supportsModulesRequest": True, - "supportsSetExpression": True, - "supportsSetVariable": True, - "supportsStepInTargetsRequest": True, - "supportsTerminateRequest": True, - "supportsValueFormattingOptions": True, - } + return adapter.CAPABILITIES_V2 def _handle_start_request(self, request: Request): if not self._is_initialized: @@ -332,15 +281,20 @@ class Adapter: def stackTrace_request(self, request: Request): thread_id = request("threadId", int) start_frame = request("startFrame", 0) + levels = request("levels", int, optional=True) thread = Thread.get(thread_id) if thread is None: raise request.isnt_valid(f'Unknown thread with "threadId":{thread_id}') + stop_frame = None if levels is None else start_frame + levels frames = None try: - frames = islice(thread.stack_trace(), start_frame, None) - return {"stackFrames": list(frames)} + frames = islice(thread.stack_trace(), start_frame, stop_frame) + return { + "stackFrames": list(frames), + "totalFrames": thread.stack_trace_len(), + } finally: del frames @@ -415,11 +369,23 @@ class Adapter: return {"scopes": frame.scopes()} def variables_request(self, request: Request): + start = request("start", 0) + count = request("count", int, optional=True) + if count == (): + count = None + filter = request("filter", str, optional=True) + match filter: + case (): + filter = {"named", "indexed"} + case "named" | "indexed": + filter = {filter} + case _: + raise request.isnt_valid(f'Invalid "filter": {filter!r}') container_id = request("variablesReference", int) container = eval.VariableContainer.get(container_id) if container is None: raise request.isnt_valid(f'Invalid "variablesReference": {container_id}') - return {"variables": list(container.variables())} + return {"variables": list(container.variables(filter, start, count))} def evaluate_request(self, request: Request): expr = request("expression", str) diff --git a/src/debugpy/server/eval.py b/src/debugpy/server/eval.py index 839d3368..06fcc76a 100644 --- a/src/debugpy/server/eval.py +++ b/src/debugpy/server/eval.py @@ -3,13 +3,16 @@ # for license information. import ctypes +import itertools import debugpy import threading -from collections.abc import Iterable +from collections.abc import Iterable, Set +from debugpy.common import log from debugpy.server.inspect import inspect -from typing import ClassVar, Dict, Self +from typing import ClassVar, Literal, Optional, Self type StackFrame = "debugpy.server.tracing.StackFrame" +type VariableFilter = Set[Literal["named", "indexed"]] _lock = threading.RLock() @@ -20,7 +23,7 @@ class VariableContainer: id: int _last_id: ClassVar[int] = 0 - _all: ClassVar[Dict[int, "VariableContainer"]] = {} + _all: ClassVar[dict[int, "VariableContainer"]] = {} def __init__(self, frame: StackFrame): self.frame = frame @@ -33,14 +36,16 @@ class VariableContainer: return {"variablesReference": self.id} def __repr__(self): - return f"{type(self).__name__}{self.__getstate__()}" + return f"{type(self).__name__}({self.id})" @classmethod def get(cls, id: int) -> Self | None: with _lock: return cls._all.get(id) - def variables(self) -> Iterable["Variable"]: + def variables( + self, filter: VariableFilter, start: int = 0, count: Optional[int] = None + ) -> Iterable["Variable"]: raise NotImplementedError def set_variable(self, name: str, value: str) -> "Value": @@ -87,8 +92,17 @@ class Value(VariableContainer): def repr(self) -> str: return "".join(inspect(self.value).repr()) - def variables(self) -> Iterable["Variable"]: - for child in inspect(self.value).children(): + def variables( + self, filter: VariableFilter, start: int = 0, count: Optional[int] = None + ) -> Iterable["Variable"]: + children = inspect(self.value).children( + include_attrs=("named" in filter), + include_items=("indexed" in filter), + ) + stop = None if count is None else start + count + log.info("Computing {0} children of {1!r} in range({2}, {3}).", filter, self, start, stop) + children = itertools.islice(children, start, stop) + for child in children: yield Variable(self.frame, child.name, child.value) def set_variable(self, name: str, value_expr: str) -> "Value": diff --git a/src/debugpy/server/inspect/__init__.py b/src/debugpy/server/inspect/__init__.py index 10d6a4cb..79953d6c 100644 --- a/src/debugpy/server/inspect/__init__.py +++ b/src/debugpy/server/inspect/__init__.py @@ -18,7 +18,7 @@ class ChildObject: def expr(self, parent_expr: str) -> str: raise NotImplementedError - + class ChildAttribute(ChildObject): name: str @@ -50,9 +50,15 @@ class ObjectInspector: except: result = "" yield result - - def children(self) -> Iterable[ChildObject]: - return sorted(self._attributes(), key=lambda var: var.name) + + def children( + self, *, include_attrs: bool = True, include_items: bool = True + ) -> Iterable[ChildObject]: + return ( + sorted(self._attributes(), key=lambda var: var.name) + if include_attrs + else () + ) def _attributes(self) -> Iterable[ChildObject]: # TODO: group class/instance/function/special @@ -87,4 +93,4 @@ def inspect(obj: object) -> ObjectInspector: case [*_] | set() | frozenset() | str() | bytes() | bytearray(): return stdlib.SequenceInspector(obj) case _: - return ObjectInspector(obj) \ No newline at end of file + return ObjectInspector(obj) diff --git a/src/debugpy/server/inspect/stdlib.py b/src/debugpy/server/inspect/stdlib.py index 98be5270..1b247b94 100644 --- a/src/debugpy/server/inspect/stdlib.py +++ b/src/debugpy/server/inspect/stdlib.py @@ -39,8 +39,14 @@ class ChildItem(ChildObject): class SequenceInspector(ObjectInspector): - def children(self) -> Iterable[ChildObject]: - yield from super().children() + def children( + self, *, include_attrs: bool = True, include_items: bool = True + ) -> Iterable[ChildObject]: + yield from super().children( + include_attrs=include_attrs, include_items=include_items + ) + if not include_items: + return yield ChildLen(self.obj) try: it = iter(self.obj) @@ -58,8 +64,14 @@ class SequenceInspector(ObjectInspector): class MappingInspector(ObjectInspector): - def children(self) -> Iterable["ChildObject"]: - yield from super().children() + def children( + self, *, include_attrs: bool = True, include_items: bool = True + ) -> Iterable[ChildObject]: + yield from super().children( + include_attrs=include_attrs, include_items=include_items + ) + if not include_items: + return yield ChildLen(self.obj) try: keys = self.obj.keys() @@ -79,7 +91,7 @@ class MappingInspector(ObjectInspector): value = exc yield ChildItem(key, value) - + class ListInspector(SequenceInspector): def repr(self) -> Iterable[str]: # TODO: move logic from SafeRepr here diff --git a/src/debugpy/server/tracing/__init__.py b/src/debugpy/server/tracing/__init__.py index 5ade5f96..f7a902e2 100644 --- a/src/debugpy/server/tracing/__init__.py +++ b/src/debugpy/server/tracing/__init__.py @@ -249,6 +249,20 @@ class Thread: ) self.is_known_to_adapter = True return True + + def stack_trace_len(self) -> int: + """ + Returns the total count of frames in this thread's stack. + """ + try: + with _cvar: + python_frame = self.current_frame + except ValueError: + raise ValueError(f"Can't get frames for inactive Thread({self.id})") + try: + return len(tuple(traceback.walk_stack(python_frame))) + finally: + del python_frame def stack_trace(self) -> Iterable["StackFrame"]: """