mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Separate out repr and children inspection.
This commit is contained in:
parent
1cb13f31e7
commit
2f4ed23203
7 changed files with 478 additions and 528 deletions
|
|
@ -368,15 +368,17 @@ class Adapter:
|
|||
return request.isnt_valid(f'Invalid "frameId": {frame_id}', silent=True)
|
||||
return {"scopes": frame.scopes()}
|
||||
|
||||
def _parse_value_format(self, request: Request) -> eval.ValueFormat:
|
||||
def _parse_value_format(
|
||||
self, request: Request, property: str = "format", max_length: int = 1024
|
||||
) -> eval.ValueFormat:
|
||||
result = eval.ValueFormat(
|
||||
hex=False,
|
||||
max_length=1024, # VSCode limit for tooltips
|
||||
max_length=max_length, # VSCode limit for tooltips
|
||||
truncation_suffix="⌇⋯",
|
||||
circular_ref_marker="↻",
|
||||
)
|
||||
|
||||
format = request("format", json.object())
|
||||
format = request(property, json.object())
|
||||
if format == {}:
|
||||
return result
|
||||
|
||||
|
|
@ -398,8 +400,12 @@ class Adapter:
|
|||
|
||||
return result
|
||||
|
||||
def _parse_name_format(self, request: Request) -> eval.ValueFormat:
|
||||
return self._parse_value_format(request, "debugpy.nameFormat", max_length=32)
|
||||
|
||||
def variables_request(self, request: Request):
|
||||
format = self._parse_value_format(request)
|
||||
name_format = self._parse_name_format(request)
|
||||
value_format = self._parse_value_format(request)
|
||||
start = request("start", 0)
|
||||
|
||||
count = request("count", int, optional=True)
|
||||
|
|
@ -420,7 +426,11 @@ class Adapter:
|
|||
if container is None:
|
||||
raise request.isnt_valid(f'Invalid "variablesReference": {container_id}')
|
||||
|
||||
return {"variables": list(container.variables(filter, format, start, count))}
|
||||
return {
|
||||
"variables": list(
|
||||
container.variables(filter, name_format, value_format, start, count)
|
||||
)
|
||||
}
|
||||
|
||||
def evaluate_request(self, request: Request):
|
||||
format = self._parse_value_format(request)
|
||||
|
|
|
|||
|
|
@ -16,9 +16,12 @@ import debugpy
|
|||
import threading
|
||||
from collections.abc import Iterable, Set
|
||||
from debugpy.common import log
|
||||
from debugpy.server.inspect import ObjectInspector, ValueFormat, inspect
|
||||
from debugpy.server.inspect import ValueFormat
|
||||
from debugpy.server.inspect.children import inspect_children
|
||||
from typing import ClassVar, Literal, Optional, Self
|
||||
|
||||
from debugpy.server.inspect.repr import formatted_repr
|
||||
|
||||
type StackFrame = "debugpy.server.tracing.StackFrame"
|
||||
type VariableFilter = Set[Literal["named", "indexed"]]
|
||||
|
||||
|
|
@ -54,7 +57,8 @@ class VariableContainer:
|
|||
def variables(
|
||||
self,
|
||||
filter: VariableFilter,
|
||||
format: ValueFormat,
|
||||
name_format: ValueFormat,
|
||||
value_format: ValueFormat,
|
||||
start: int = 0,
|
||||
count: Optional[int] = None,
|
||||
) -> Iterable["Variable"]:
|
||||
|
|
@ -86,21 +90,18 @@ class Value(VariableContainer):
|
|||
self.format = format
|
||||
|
||||
def __getstate__(self) -> dict[str, object]:
|
||||
inspector = inspect_children(self.value)
|
||||
state = super().__getstate__()
|
||||
state.update(
|
||||
{
|
||||
"type": self.typename,
|
||||
"value": self.repr(),
|
||||
"namedVariables": self.inspector.named_children_count(),
|
||||
"indexedVariables": self.inspector.indexed_children_count(),
|
||||
"value": formatted_repr(self.value, self.format),
|
||||
"namedVariables": inspector.named_children_count(),
|
||||
"indexedVariables": inspector.indexed_children_count(),
|
||||
}
|
||||
)
|
||||
return state
|
||||
|
||||
@property
|
||||
def inspector(self) -> ObjectInspector:
|
||||
return inspect(self.value, self.format)
|
||||
|
||||
@property
|
||||
def typename(self) -> str:
|
||||
try:
|
||||
|
|
@ -108,13 +109,11 @@ class Value(VariableContainer):
|
|||
except:
|
||||
return ""
|
||||
|
||||
def repr(self) -> str:
|
||||
return self.inspector.repr()
|
||||
|
||||
def variables(
|
||||
self,
|
||||
filter: VariableFilter,
|
||||
format: ValueFormat,
|
||||
name_format: ValueFormat,
|
||||
value_format: ValueFormat,
|
||||
start: int = 0,
|
||||
count: Optional[int] = None,
|
||||
) -> Iterable["Variable"]:
|
||||
|
|
@ -127,14 +126,16 @@ class Value(VariableContainer):
|
|||
stop,
|
||||
)
|
||||
|
||||
inspector = inspect(self.value, format)
|
||||
inspector = inspect_children(self.value)
|
||||
children = itertools.chain(
|
||||
inspector.named_children() if "named" in filter else (),
|
||||
inspector.indexed_children() if "indexed" in filter else (),
|
||||
)
|
||||
children = itertools.islice(children, start, stop)
|
||||
for child in children:
|
||||
yield Variable(self.frame, child.accessor(format), child.value, format)
|
||||
yield Variable(
|
||||
self.frame, child.accessor(name_format), child.value, value_format
|
||||
)
|
||||
|
||||
def set_variable(self, name: str, value_expr: str, format: ValueFormat) -> "Value":
|
||||
value = self.frame.evaluate(value_expr)
|
||||
|
|
@ -160,7 +161,9 @@ class Variable(Value):
|
|||
name: str
|
||||
# TODO: evaluateName
|
||||
|
||||
def __init__(self, frame: StackFrame, name: str, value: object, format: ValueFormat):
|
||||
def __init__(
|
||||
self, frame: StackFrame, name: str, value: object, format: ValueFormat
|
||||
):
|
||||
super().__init__(frame, value, format)
|
||||
self.name = name
|
||||
|
||||
|
|
|
|||
|
|
@ -12,320 +12,29 @@ debugpy.server.eval then wraps it in DAP-specific adapter classes that expose th
|
|||
same functionality in DAP terms.
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
from array import array
|
||||
from collections import deque
|
||||
from collections.abc import Iterable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValueFormat:
|
||||
hex: bool
|
||||
hex: bool = False
|
||||
"""Whether integers should be rendered in hexadecimal."""
|
||||
|
||||
max_length: int
|
||||
max_length: int = sys.maxsize
|
||||
"""
|
||||
Maximum length of the string representation of variable values, including values
|
||||
of indices returned by IndexedChildObject.accessor().
|
||||
Maximum length of the string representation of variable values.
|
||||
"""
|
||||
|
||||
truncation_suffix: str
|
||||
"""Suffix to append to truncated string representations; counts towards max_length."""
|
||||
truncation_suffix: str = ""
|
||||
"""Suffix to append to string representation of value when truncation occurs.
|
||||
Counts towards max_value_length and max_key_length."""
|
||||
|
||||
circular_ref_marker: Optional[str]
|
||||
circular_ref_marker: Optional[str] = None
|
||||
"""
|
||||
String to use for nested circular references (e.g. list containing itself). If None,
|
||||
circular references aren't detected and the caller is responsible for avoiding them
|
||||
in inputs.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
hex: bool = False,
|
||||
max_length: int = sys.maxsize,
|
||||
truncation_suffix: str = "",
|
||||
circular_ref_marker: Optional[str] = None,
|
||||
):
|
||||
assert max_length >= len(truncation_suffix)
|
||||
self.hex = hex
|
||||
self.max_length = max_length
|
||||
self.truncation_suffix = truncation_suffix
|
||||
self.circular_ref_marker = circular_ref_marker
|
||||
|
||||
|
||||
class ChildObject:
|
||||
"""
|
||||
Represents an object that is a child of another object that is accessible in some way.
|
||||
"""
|
||||
|
||||
value: object
|
||||
|
||||
def __init__(self, value: object):
|
||||
self.value = value
|
||||
self.format = format
|
||||
|
||||
def accessor(self, format: ValueFormat) -> str:
|
||||
"""
|
||||
Accessor used to retrieve this object.
|
||||
|
||||
This is a display string and is not intended to be used for eval, but it should
|
||||
generally correlate to the expression that can be used to retrieve the object in
|
||||
some clear and obvious way. Some examples of accessors:
|
||||
|
||||
"attr" - value.attr
|
||||
"[key]" - value[key]
|
||||
"len()" - len(value)
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def expr(self, parent_expr: str) -> str:
|
||||
"""
|
||||
Returns an expression that can be used to retrieve this object from its parent,
|
||||
given the expression to compute the parent.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class NamedChildObject(ChildObject):
|
||||
"""
|
||||
Child object that has a predefined accessor used to access it.
|
||||
|
||||
This includes not just attributes, but all children that do not require repr() of
|
||||
index, key etc to compute the accessor.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, value: object):
|
||||
super().__init__(value)
|
||||
self.name = name
|
||||
|
||||
def accessor(self, format: ValueFormat) -> str:
|
||||
return self.name
|
||||
|
||||
def expr(self, parent_expr: str) -> str:
|
||||
accessor = self.accessor(ValueFormat())
|
||||
return f"({parent_expr}).{accessor}"
|
||||
|
||||
|
||||
class LenChildObject(NamedChildObject):
|
||||
"""
|
||||
A synthetic child object that represents the return value of len().
|
||||
"""
|
||||
|
||||
def __init__(self, parent: object):
|
||||
super().__init__("len()", len(parent))
|
||||
|
||||
def expr(self, parent_expr: str) -> str:
|
||||
return f"len({parent_expr})"
|
||||
|
||||
|
||||
class IndexedChildObject(ChildObject):
|
||||
"""
|
||||
Child object that has a computed accessor.
|
||||
"""
|
||||
|
||||
key: object
|
||||
|
||||
def __init__(self, key: object, value: object):
|
||||
super().__init__(value)
|
||||
self.key = key
|
||||
self.indexer = None
|
||||
|
||||
def accessor(self, format: ValueFormat) -> str:
|
||||
key_repr = inspect(self.key, format).repr()
|
||||
return f"[{key_repr}]"
|
||||
|
||||
def expr(self, parent_expr: str) -> str:
|
||||
accessor = self.accessor(ValueFormat())
|
||||
return f"({parent_expr}){accessor}"
|
||||
|
||||
|
||||
# TODO: break apart into separate classes for child inspection and for repr, because these
|
||||
# don't necessarily match. For example, if a user-defined class is derived from dict, the
|
||||
# protocol to retrieve the children is still the same, so the same inspector should be used
|
||||
# for it. However, its repr will likely be different, and if we use the dict inspector for
|
||||
# any subclass of dict, we'll get this wrong. This matters when editing variable values,
|
||||
# since repr of the value provides the initial text for the user to edit. So if we show a
|
||||
# dict repr for a subclass, and user clicks edit and then saves, the value will be silently
|
||||
# replaced with a plain dict.
|
||||
class ObjectInspector:
|
||||
"""
|
||||
Inspects a generic object, providing access to its string representation and children.
|
||||
"""
|
||||
|
||||
class ReprContext:
|
||||
"""
|
||||
Context for ObjectInspector.iter_repr().
|
||||
"""
|
||||
|
||||
format: ValueFormat
|
||||
|
||||
chars_remaining: int
|
||||
"""
|
||||
How many more characters are allowed in the output.
|
||||
|
||||
Implementations of ObjectInspector.iter_repr() can use this to optimize by yielding
|
||||
larger chunks if there is enough space left for them.
|
||||
"""
|
||||
|
||||
path: list[object]
|
||||
"""
|
||||
Path to the current object being inspected, starting from the root object on which
|
||||
repr() was called, with each new element corresponding to a single nest() call.
|
||||
"""
|
||||
|
||||
def __init__(self, inspector: "ObjectInspector"):
|
||||
self.format = inspector.format
|
||||
self.chars_remaining = self.format.max_length
|
||||
self.path = []
|
||||
|
||||
def nest(self, value: object):
|
||||
circular_ref_marker = self.format.circular_ref_marker
|
||||
if circular_ref_marker is not None and any(x is value for x in self.path):
|
||||
yield circular_ref_marker
|
||||
return
|
||||
|
||||
self.path.append(value)
|
||||
try:
|
||||
yield from inspect(value, self.format).iter_repr(self)
|
||||
finally:
|
||||
self.path.pop()
|
||||
|
||||
value: object
|
||||
format: ValueFormat
|
||||
|
||||
def __init__(self, value: object, format: ValueFormat):
|
||||
self.value = value
|
||||
self.format = format
|
||||
|
||||
def children(self) -> Iterable[ChildObject]:
|
||||
yield from self.named_children()
|
||||
yield from self.indexed_children()
|
||||
|
||||
def indexed_children_count(self) -> int:
|
||||
try:
|
||||
return len(self.value)
|
||||
except:
|
||||
return 0
|
||||
|
||||
def indexed_children(self) -> Iterable[IndexedChildObject]:
|
||||
return ()
|
||||
|
||||
def named_children_count(self) -> int:
|
||||
return len(tuple(self.named_children()))
|
||||
|
||||
def named_children(self) -> Iterable[NamedChildObject]:
|
||||
def attrs():
|
||||
try:
|
||||
names = dir(self.value)
|
||||
except:
|
||||
names = ()
|
||||
|
||||
# TODO: group class/instance/function/special
|
||||
for name in names:
|
||||
if name.startswith("__"):
|
||||
continue
|
||||
try:
|
||||
value = getattr(self.value, name)
|
||||
except BaseException as exc:
|
||||
value = exc
|
||||
try:
|
||||
if hasattr(value, "__call__"):
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
yield NamedChildObject(name, value)
|
||||
|
||||
try:
|
||||
yield LenChildObject(self.value)
|
||||
except:
|
||||
pass
|
||||
|
||||
return sorted(attrs(), key=lambda var: var.name)
|
||||
|
||||
def repr(self) -> str:
|
||||
"""
|
||||
repr() of the inspected object. Like builtins.repr(), but with additional
|
||||
formatting options and size limit.
|
||||
"""
|
||||
context = self.ReprContext(self)
|
||||
output = io.StringIO()
|
||||
for chunk in context.nest(self.value):
|
||||
output.write(chunk)
|
||||
context.chars_remaining -= len(chunk)
|
||||
if context.chars_remaining < 0:
|
||||
output.seek(self.format.max_length - len(self.format.truncation_suffix))
|
||||
output.truncate()
|
||||
output.write(self.format.truncation_suffix)
|
||||
break
|
||||
return output.getvalue()
|
||||
|
||||
def iter_repr(self, context: ReprContext) -> Iterable[str]:
|
||||
"""
|
||||
Streaming repr of the inspected object. Like builtins.repr(), but instead
|
||||
of computing and returning the whole string right away, returns an iterator
|
||||
that yields chunks of the repr as they are computed.
|
||||
|
||||
When object being inspected contains other objects that it needs to include
|
||||
in its own repr, it should pass the nested objects to context.nest() and
|
||||
yield from the returned iterator. This will dispatch the nested repr to the
|
||||
correct inspector, and make sure that context.nesting_level is updated as
|
||||
needed while nested repr is being computed.
|
||||
|
||||
When possible, implementations should use context.chars_remaining as a hint
|
||||
to yield larger chunks. However, there is no obligation for iter_repr() to
|
||||
yield chunks smaller than chars_remaining.
|
||||
|
||||
The default implementation delegates to builtins.repr(), which will always
|
||||
produce the correct result, but without any streaming. Derived inspectors
|
||||
should always override this method to stream repr if possible.
|
||||
"""
|
||||
try:
|
||||
result = repr(self.value)
|
||||
except BaseException as exc:
|
||||
try:
|
||||
result = f"<repr() error: {exc}>"
|
||||
except:
|
||||
result = "<repr() error>"
|
||||
yield result
|
||||
|
||||
|
||||
def inspect(value: object, format: ValueFormat) -> ObjectInspector:
|
||||
from debugpy.server.inspect import stdlib
|
||||
|
||||
# TODO: proper extensible registry with public API for debugpy plugins.
|
||||
def get_inspector():
|
||||
# TODO: should subtypes of standard collections be treated the same? This works
|
||||
# for fetching items, but gets repr() wrong - might have to split the two.
|
||||
match value:
|
||||
case int():
|
||||
return stdlib.IntInspector
|
||||
case str():
|
||||
return stdlib.StrInspector
|
||||
case bytes():
|
||||
return stdlib.BytesInspector
|
||||
case bytearray():
|
||||
return stdlib.ByteArrayInspector
|
||||
case tuple():
|
||||
return stdlib.TupleInspector
|
||||
case list():
|
||||
return stdlib.ListInspector
|
||||
case set():
|
||||
return stdlib.SetInspector
|
||||
case frozenset():
|
||||
return stdlib.FrozenSetInspector
|
||||
case array():
|
||||
return stdlib.ArrayInspector
|
||||
case deque():
|
||||
return stdlib.DequeInspector
|
||||
case dict():
|
||||
return stdlib.DictInspector
|
||||
case Mapping():
|
||||
return stdlib.MappingInspector
|
||||
case Iterable():
|
||||
return stdlib.IterableInspector
|
||||
case _:
|
||||
return ObjectInspector
|
||||
|
||||
return get_inspector()(value, format)
|
||||
|
||||
217
src/debugpy/server/inspect/children.py
Normal file
217
src/debugpy/server/inspect/children.py
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See LICENSE in the project root
|
||||
# for license information.
|
||||
|
||||
import dataclasses
|
||||
from collections.abc import Iterable, Mapping
|
||||
from debugpy.common import log
|
||||
from debugpy.server.inspect import ValueFormat
|
||||
from debugpy.server.inspect.repr import formatted_repr
|
||||
from itertools import count
|
||||
|
||||
|
||||
class ChildObject:
|
||||
"""
|
||||
Represents an object that is a child of another object that is accessible in some way.
|
||||
"""
|
||||
|
||||
value: object
|
||||
|
||||
def __init__(self, value: object):
|
||||
self.value = value
|
||||
|
||||
def accessor(self, format: ValueFormat) -> str:
|
||||
"""
|
||||
Accessor used to retrieve this object.
|
||||
|
||||
This is a display string and is not intended to be used for eval, but it should
|
||||
generally correlate to the expression that can be used to retrieve the object in
|
||||
some clear and obvious way. Some examples of accessors:
|
||||
|
||||
"attr" - value.attr
|
||||
"[key]" - value[key]
|
||||
"len()" - len(value)
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def expr(self, parent_expr: str) -> str:
|
||||
"""
|
||||
Returns an expression that can be used to retrieve this object from its parent,
|
||||
given the expression to compute the parent.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class NamedChildObject(ChildObject):
|
||||
"""
|
||||
Child object that has a predefined accessor used to access it.
|
||||
|
||||
This includes not just attributes, but all children that do not require repr() of
|
||||
index, key etc to compute the accessor.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, value: object):
|
||||
super().__init__(value)
|
||||
self.name = name
|
||||
|
||||
def accessor(self, format: ValueFormat) -> str:
|
||||
return self.name
|
||||
|
||||
def expr(self, parent_expr: str) -> str:
|
||||
accessor = self.accessor(ValueFormat())
|
||||
return f"({parent_expr}).{accessor}"
|
||||
|
||||
|
||||
class LenChildObject(NamedChildObject):
|
||||
"""
|
||||
A synthetic child object that represents the return value of len().
|
||||
"""
|
||||
|
||||
def __init__(self, parent: object):
|
||||
super().__init__("len()", len(parent))
|
||||
|
||||
def expr(self, parent_expr: str) -> str:
|
||||
return f"len({parent_expr})"
|
||||
|
||||
|
||||
class IndexedChildObject(ChildObject):
|
||||
"""
|
||||
Child object that has a computed accessor.
|
||||
"""
|
||||
|
||||
key: object
|
||||
|
||||
def __init__(self, key: object, value: object):
|
||||
super().__init__(value)
|
||||
self.key = key
|
||||
self.indexer = None
|
||||
|
||||
def accessor(self, format: ValueFormat) -> str:
|
||||
key_format = dataclasses.replace(format, max_length=format.max_length - 2)
|
||||
key_repr = formatted_repr(self.key, key_format)
|
||||
return f"[{key_repr}]"
|
||||
|
||||
def expr(self, parent_expr: str) -> str:
|
||||
accessor = self.accessor(ValueFormat())
|
||||
return f"({parent_expr}){accessor}"
|
||||
|
||||
|
||||
class ObjectInspector:
|
||||
"""
|
||||
Inspects a generic object, providing access to its children (attributes, items etc).
|
||||
"""
|
||||
|
||||
value: object
|
||||
|
||||
def __init__(self, value: object):
|
||||
self.value = value
|
||||
|
||||
def children(self) -> Iterable[ChildObject]:
|
||||
yield from self.named_children()
|
||||
yield from self.indexed_children()
|
||||
|
||||
def indexed_children_count(self) -> int:
|
||||
try:
|
||||
return len(self.value)
|
||||
except:
|
||||
return 0
|
||||
|
||||
def indexed_children(self) -> Iterable[IndexedChildObject]:
|
||||
return ()
|
||||
|
||||
def named_children_count(self) -> int:
|
||||
return len(tuple(self.named_children()))
|
||||
|
||||
def named_children(self) -> Iterable[NamedChildObject]:
|
||||
def attrs():
|
||||
try:
|
||||
names = dir(self.value)
|
||||
except:
|
||||
names = ()
|
||||
|
||||
# TODO: group class/instance/function/special
|
||||
for name in names:
|
||||
if name.startswith("__"):
|
||||
continue
|
||||
try:
|
||||
value = getattr(self.value, name)
|
||||
except BaseException as exc:
|
||||
value = exc
|
||||
try:
|
||||
if hasattr(value, "__call__"):
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
yield NamedChildObject(name, value)
|
||||
|
||||
try:
|
||||
yield LenChildObject(self.value)
|
||||
except:
|
||||
pass
|
||||
|
||||
return sorted(attrs(), key=lambda var: var.name)
|
||||
|
||||
|
||||
class IterableInspector(ObjectInspector):
|
||||
value: Iterable
|
||||
|
||||
def indexed_children(self) -> Iterable[IndexedChildObject]:
|
||||
yield from super().indexed_children()
|
||||
try:
|
||||
it = iter(self.value)
|
||||
except:
|
||||
return
|
||||
for i in count():
|
||||
try:
|
||||
item = next(it)
|
||||
except StopIteration:
|
||||
break
|
||||
except:
|
||||
log.exception("Error retrieving next item.")
|
||||
break
|
||||
yield IndexedChildObject(i, item)
|
||||
|
||||
|
||||
class MappingInspector(ObjectInspector):
|
||||
value: Mapping
|
||||
|
||||
def indexed_children(self) -> Iterable[IndexedChildObject]:
|
||||
yield from super().indexed_children()
|
||||
try:
|
||||
keys = self.value.keys()
|
||||
except:
|
||||
return
|
||||
it = iter(keys)
|
||||
while True:
|
||||
try:
|
||||
key = next(it)
|
||||
except StopIteration:
|
||||
break
|
||||
except:
|
||||
break
|
||||
try:
|
||||
value = self.value[key]
|
||||
except BaseException as exc:
|
||||
value = exc
|
||||
yield IndexedChildObject(key, value)
|
||||
|
||||
|
||||
# Indexing str yields str, which is not very useful for debugging. What we want is to
|
||||
# show the ordinal character values, similar to how it works for bytes & bytearray.
|
||||
class StrInspector(IterableInspector):
|
||||
def indexed_children(self) -> Iterable[IndexedChildObject]:
|
||||
for i, ch in enumerate(self.value):
|
||||
yield IndexedChildObject(i, ord(ch))
|
||||
|
||||
|
||||
def inspect_children(value: object) -> ObjectInspector:
|
||||
# TODO: proper extensible registry with public API for debugpy plugins.
|
||||
match value:
|
||||
case str():
|
||||
return StrInspector(value)
|
||||
case Mapping():
|
||||
return MappingInspector(value)
|
||||
case Iterable():
|
||||
return IterableInspector(value)
|
||||
case _:
|
||||
return ObjectInspector(value)
|
||||
215
src/debugpy/server/inspect/repr.py
Normal file
215
src/debugpy/server/inspect/repr.py
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See LICENSE in the project root
|
||||
# for license information.
|
||||
|
||||
import functools
|
||||
import io
|
||||
from collections.abc import Iterable, Mapping
|
||||
from typing import Callable
|
||||
from debugpy.server.inspect import ValueFormat
|
||||
|
||||
|
||||
class ReprTooLongError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ReprBuilder:
|
||||
output: io.StringIO
|
||||
|
||||
format: ValueFormat
|
||||
|
||||
path: list[object]
|
||||
"""
|
||||
Path to the current object being inspected, starting from the root object on which
|
||||
repr() was called, with each new element corresponding to a single nest() call.
|
||||
"""
|
||||
|
||||
chars_remaining: int
|
||||
"""
|
||||
How many more characters are allowed in the output.
|
||||
|
||||
Formatters can use this to optimize by appending larger chunks if there is enough
|
||||
space left for them. However, this is just a hint, and formatters aren't required
|
||||
to truncate their output - the ReprBuilder will take care of that automatically.
|
||||
"""
|
||||
|
||||
def __init__(self, format: ValueFormat):
|
||||
self.output = io.StringIO()
|
||||
self.format = format
|
||||
self.path = []
|
||||
self.chars_remaining = self.format.max_length
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.output.getvalue()
|
||||
|
||||
def append_text(self, text: str):
|
||||
self.output.write(text)
|
||||
self.chars_remaining -= len(text)
|
||||
if self.chars_remaining < 0:
|
||||
self.output.seek(
|
||||
self.format.max_length - len(self.format.truncation_suffix)
|
||||
)
|
||||
self.output.truncate()
|
||||
self.output.write(self.format.truncation_suffix)
|
||||
raise ReprTooLongError
|
||||
|
||||
def append_object(self, value: object):
|
||||
circular_ref_marker = self.format.circular_ref_marker
|
||||
if circular_ref_marker is not None and any(x is value for x in self.path):
|
||||
self.append_text(circular_ref_marker)
|
||||
return
|
||||
|
||||
formatter = get_formatter(value)
|
||||
self.path.append(value)
|
||||
try:
|
||||
formatter(value, self)
|
||||
finally:
|
||||
self.path.pop()
|
||||
|
||||
|
||||
def format_object(value: object, builder: ReprBuilder):
|
||||
try:
|
||||
result = repr(value)
|
||||
except BaseException as exc:
|
||||
try:
|
||||
result = f"<repr() error: {exc}>"
|
||||
except:
|
||||
result = "<repr() error>"
|
||||
builder.append_text(result)
|
||||
|
||||
|
||||
def format_int(value: int, builder: ReprBuilder):
|
||||
fs = "{:#x}" if builder.format.hex else "{}"
|
||||
text = fs.format(value)
|
||||
builder.append_text(text)
|
||||
|
||||
|
||||
def format_iterable(
|
||||
value: Iterable, builder: ReprBuilder, *, prefix: str = None, suffix: str = None
|
||||
):
|
||||
if prefix is None:
|
||||
prefix = type(value).__name__ + "(("
|
||||
builder.append_text(prefix)
|
||||
|
||||
for i, item in enumerate(value):
|
||||
if i > 0:
|
||||
builder.append_text(", ")
|
||||
builder.append_object(item)
|
||||
|
||||
if suffix is None:
|
||||
suffix = "))"
|
||||
builder.append_text(suffix)
|
||||
|
||||
|
||||
def format_mapping(
|
||||
value: Mapping, builder: ReprBuilder, *, prefix: str = None, suffix: str = None
|
||||
):
|
||||
if prefix is None:
|
||||
prefix = type(value).__name__ + "(("
|
||||
builder.append_text(prefix)
|
||||
|
||||
for i, (key, value) in enumerate(value.items()):
|
||||
if i > 0:
|
||||
builder.append_text(", ")
|
||||
builder.append_object(key)
|
||||
builder.append_text(": ")
|
||||
builder.append_object(value)
|
||||
|
||||
if suffix is None:
|
||||
suffix = "))"
|
||||
builder.append_text(suffix)
|
||||
|
||||
|
||||
def format_strlike(
|
||||
value: str | bytes | bytearray, builder: ReprBuilder, *, prefix: str, suffix: str
|
||||
):
|
||||
builder.append_text(prefix)
|
||||
|
||||
i = 0
|
||||
while i < len(value):
|
||||
# Optimistically assume that no escaping will be needed.
|
||||
chunk_size = max(1, builder.chars_remaining)
|
||||
chunk = repr(value[i : i + chunk_size])
|
||||
chunk = chunk[len(prefix) : -len(suffix)]
|
||||
builder.append_text(chunk)
|
||||
i += chunk_size
|
||||
|
||||
builder.append_text(suffix)
|
||||
|
||||
|
||||
def format_tuple(value: tuple, builder: ReprBuilder):
|
||||
suffix = ",)" if len(value) == 1 else ")"
|
||||
format_iterable(value, builder, prefix="(", suffix=suffix)
|
||||
|
||||
|
||||
format_str = functools.partial(format_strlike, prefix="'", suffix="'")
|
||||
|
||||
format_bytes = functools.partial(format_strlike, prefix="b'", suffix="'")
|
||||
|
||||
format_bytearray = functools.partial(format_strlike, prefix="bytearray(b'", suffix="')")
|
||||
|
||||
format_list = functools.partial(format_iterable, prefix="[", suffix="]")
|
||||
|
||||
format_set = functools.partial(format_iterable, prefix="{", suffix="}")
|
||||
|
||||
format_frozenset = functools.partial(format_iterable, prefix="frozenset({", suffix="})")
|
||||
|
||||
format_dict = functools.partial(format_mapping, prefix="{", suffix="}")
|
||||
|
||||
|
||||
type Formatter = Callable[[object, ReprBuilder]]
|
||||
|
||||
formatters: Mapping[type, Formatter] = {
|
||||
int: format_int,
|
||||
str: format_str,
|
||||
bytes: format_bytes,
|
||||
bytearray: format_bytearray,
|
||||
tuple: format_tuple,
|
||||
list: format_list,
|
||||
set: format_set,
|
||||
frozenset: format_frozenset,
|
||||
dict: format_dict,
|
||||
}
|
||||
|
||||
|
||||
def get_formatter(value: object) -> Formatter:
|
||||
# TODO: proper extensible registry with public API for debugpy plugins.
|
||||
|
||||
# First let's see if we have a formatter for this specific type. Matching on type
|
||||
# here must be exact, i.e. no subtypes. The reason for this is that repr must,
|
||||
# insofar as possible, reconstitute the original object if eval'd; but if we use
|
||||
# a base class repr for a subclass, evaling it will produce instance of that base
|
||||
# class instead. This matters when editing variable values, since repr of the value
|
||||
# is the text that user will be editing. So if we show a dict repr for a subclass
|
||||
# of dict, and user edits it and saves, the value will be silently overwritten with
|
||||
# a plain dict. To avoid data loss, we must always use generic repr in cases where
|
||||
# we don't know the type exactly.
|
||||
formatter = formatters.get(type(value), None)
|
||||
if formatter is not None:
|
||||
return formatter
|
||||
|
||||
# If there's no specific formatter for this type, pick a generic formatter instead.
|
||||
# For this, we do want subtype matching, because those generic formatters produce
|
||||
# repr that includes the type name following the standard pattern for those types -
|
||||
# so e.g. a Sequence of type T will be formatted as "T((items))".
|
||||
match value:
|
||||
case Mapping():
|
||||
return format_mapping
|
||||
case Iterable():
|
||||
return format_iterable
|
||||
case _:
|
||||
return format_object
|
||||
|
||||
|
||||
def formatted_repr(value: object, format: ValueFormat) -> str:
|
||||
builder = ReprBuilder(format)
|
||||
try:
|
||||
builder.append_object(value)
|
||||
except ReprTooLongError:
|
||||
pass
|
||||
except BaseException as exc:
|
||||
try:
|
||||
builder.append_text(f"<error: {exc}>")
|
||||
except:
|
||||
builder.append_text("<error>")
|
||||
return str(builder)
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See LICENSE in the project root
|
||||
# for license information.
|
||||
|
||||
"""Object inspection for builtin Python types."""
|
||||
|
||||
from collections.abc import Iterable, Mapping
|
||||
from itertools import count
|
||||
|
||||
from debugpy.common import log
|
||||
from debugpy.server.inspect import ObjectInspector, IndexedChildObject
|
||||
|
||||
|
||||
class IterableInspector(ObjectInspector):
|
||||
value: Iterable
|
||||
|
||||
def indexed_children(self) -> Iterable[IndexedChildObject]:
|
||||
yield from super().indexed_children()
|
||||
try:
|
||||
it = iter(self.value)
|
||||
except:
|
||||
return
|
||||
for i in count():
|
||||
try:
|
||||
item = next(it)
|
||||
except StopIteration:
|
||||
break
|
||||
except:
|
||||
log.exception("Error retrieving next item.")
|
||||
break
|
||||
yield IndexedChildObject(i, item)
|
||||
|
||||
|
||||
class MappingInspector(ObjectInspector):
|
||||
value: Mapping
|
||||
|
||||
def indexed_children(self) -> Iterable[IndexedChildObject]:
|
||||
yield from super().indexed_children()
|
||||
try:
|
||||
keys = self.value.keys()
|
||||
except:
|
||||
return
|
||||
it = iter(keys)
|
||||
while True:
|
||||
try:
|
||||
key = next(it)
|
||||
except StopIteration:
|
||||
break
|
||||
except:
|
||||
break
|
||||
try:
|
||||
value = self.value[key]
|
||||
except BaseException as exc:
|
||||
value = exc
|
||||
yield IndexedChildObject(key, value)
|
||||
|
||||
|
||||
class IterableInspectorWithRepr(IterableInspector):
|
||||
def repr_prefix(self) -> str:
|
||||
return type(self.value).__name__ + "(("
|
||||
|
||||
def repr_suffix(self) -> str:
|
||||
return "))"
|
||||
|
||||
def repr_items(self) -> Iterable[object]:
|
||||
return self.value
|
||||
|
||||
def iter_repr(self, context: ObjectInspector.ReprContext) -> Iterable[str]:
|
||||
yield self.repr_prefix()
|
||||
for i, item in enumerate(self.value):
|
||||
if i > 0:
|
||||
yield ", "
|
||||
yield from context.nest(item)
|
||||
yield self.repr_suffix()
|
||||
|
||||
|
||||
class MappingInspectorWithRepr(MappingInspector):
|
||||
def repr_prefix(self) -> str:
|
||||
return type(self.value).__name__ + "({"
|
||||
|
||||
def repr_suffix(self) -> str:
|
||||
return "})"
|
||||
|
||||
def iter_repr(self, context: ObjectInspector.ReprContext) -> Iterable[str]:
|
||||
yield self.repr_prefix()
|
||||
for i, (key, value) in enumerate(self.value.items()):
|
||||
if i > 0:
|
||||
yield ", "
|
||||
yield from context.nest(key)
|
||||
yield ": "
|
||||
yield from context.nest(value)
|
||||
yield self.repr_suffix()
|
||||
|
||||
|
||||
class StrLikeInspector(IterableInspector):
|
||||
value: str | bytes | bytearray
|
||||
|
||||
def repr_prefix(self) -> str:
|
||||
return "'"
|
||||
|
||||
def repr_suffix(self) -> str:
|
||||
return "'"
|
||||
|
||||
def indexed_children(self) -> Iterable[IndexedChildObject]:
|
||||
if isinstance(self.value, str):
|
||||
# Indexing str yields str, which is not very useful for debugging.
|
||||
# What we want is to show the ordinal character values, similar
|
||||
# to how it works for bytes & bytearray.
|
||||
for i, ch in enumerate(self.value):
|
||||
yield IndexedChildObject(i, ord(ch))
|
||||
else:
|
||||
yield from super().indexed_children()
|
||||
|
||||
def iter_repr(self, context: ObjectInspector.ReprContext) -> Iterable[str]:
|
||||
prefix = self.repr_prefix()
|
||||
suffix = self.repr_suffix()
|
||||
yield prefix
|
||||
i = 0
|
||||
while i < len(self.value):
|
||||
# Optimistically assume that no escaping will be needed.
|
||||
chunk_size = max(1, context.chars_remaining)
|
||||
chunk = repr(self.value[i : i + chunk_size])
|
||||
yield chunk[len(prefix) : -len(suffix)]
|
||||
i += chunk_size
|
||||
yield suffix
|
||||
|
||||
|
||||
class IntInspector(ObjectInspector):
|
||||
value: int
|
||||
|
||||
def iter_repr(self, context: ObjectInspector.ReprContext) -> Iterable[str]:
|
||||
fs = "{:#x}" if self.format.hex else "{}"
|
||||
yield fs.format(self.value)
|
||||
|
||||
|
||||
class BytesInspector(StrLikeInspector):
|
||||
def repr_prefix(self) -> str:
|
||||
return "b'"
|
||||
|
||||
|
||||
class ByteArrayInspector(StrLikeInspector):
|
||||
def repr_prefix(self) -> str:
|
||||
return "bytearray(b'"
|
||||
|
||||
def repr_suffix(self) -> str:
|
||||
return "')"
|
||||
|
||||
|
||||
class StrInspector(StrLikeInspector):
|
||||
def indexed_children(self) -> Iterable[IndexedChildObject]:
|
||||
# Indexing str yields str, which is not very useful for debugging. We want
|
||||
# to show the ordinal character values, similar to how it works for bytes.
|
||||
for i, ch in enumerate(self.value):
|
||||
yield IndexedChildObject(i, ord(ch))
|
||||
|
||||
|
||||
class ListInspector(IterableInspectorWithRepr):
|
||||
def repr_prefix(self) -> str:
|
||||
return "["
|
||||
|
||||
def repr_suffix(self) -> str:
|
||||
return "]"
|
||||
|
||||
|
||||
class TupleInspector(IterableInspectorWithRepr):
|
||||
def repr_prefix(self) -> str:
|
||||
return "("
|
||||
|
||||
def repr_suffix(self) -> str:
|
||||
return ",)" if len(self.value) == 1 else ")"
|
||||
|
||||
|
||||
class SetInspector(IterableInspectorWithRepr):
|
||||
def repr_prefix(self) -> str:
|
||||
return "{"
|
||||
|
||||
def repr_suffix(self) -> str:
|
||||
return "}"
|
||||
|
||||
|
||||
class FrozenSetInspector(IterableInspectorWithRepr):
|
||||
def repr_prefix(self) -> str:
|
||||
return "frozenset({"
|
||||
|
||||
def repr_suffix(self) -> str:
|
||||
return "})"
|
||||
|
||||
|
||||
class ArrayInspector(IterableInspectorWithRepr):
|
||||
def repr_prefix(self) -> str:
|
||||
return f"array({self.value.typecode!r}, ("
|
||||
|
||||
def repr_suffix(self) -> str:
|
||||
return "))"
|
||||
|
||||
|
||||
class DequeInspector(IterableInspectorWithRepr):
|
||||
pass
|
||||
|
||||
|
||||
class DictInspector(MappingInspectorWithRepr):
|
||||
def repr_prefix(self) -> str:
|
||||
return "{"
|
||||
|
||||
def repr_suffix(self) -> str:
|
||||
return "}"
|
||||
|
|
@ -385,6 +385,8 @@ class Step:
|
|||
return f"Step({self.step})"
|
||||
|
||||
def is_complete(self, python_frame: FrameType) -> bool:
|
||||
# TODO: avoid using traceback.walk_stack every time by counting stack
|
||||
# depth via PY_CALL / PY_RETURN events.
|
||||
is_complete = False
|
||||
if self.step == "in":
|
||||
is_complete = (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue