Variable paging

This commit is contained in:
Pavel Minaev 2024-03-28 17:01:13 -07:00
parent c50c59b40c
commit c4fdc79936
8 changed files with 153 additions and 121 deletions

View file

@ -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."""

View file

@ -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.
#

View file

@ -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)

View file

@ -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)

View file

@ -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":

View file

@ -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 = "<repr() error>"
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)
return ObjectInspector(obj)

View file

@ -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

View file

@ -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"]:
"""