Separate out repr and children inspection.

This commit is contained in:
Pavel Minaev 2024-04-18 12:53:08 -07:00 committed by Pavel Minaev
parent 1cb13f31e7
commit 2f4ed23203
7 changed files with 478 additions and 528 deletions

View file

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

View file

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

View file

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

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

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

View file

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

View file

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