mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Variable paging
This commit is contained in:
parent
c50c59b40c
commit
c4fdc79936
8 changed files with 153 additions and 121 deletions
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]:
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue