diff --git a/src/debugpy/server/adapters.py b/src/debugpy/server/adapters.py index 69aa8714..d34d1c40 100644 --- a/src/debugpy/server/adapters.py +++ b/src/debugpy/server/adapters.py @@ -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) diff --git a/src/debugpy/server/eval.py b/src/debugpy/server/eval.py index 01b2b80f..522d565a 100644 --- a/src/debugpy/server/eval.py +++ b/src/debugpy/server/eval.py @@ -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 diff --git a/src/debugpy/server/inspect/__init__.py b/src/debugpy/server/inspect/__init__.py index 5c4be12b..03da344f 100644 --- a/src/debugpy/server/inspect/__init__.py +++ b/src/debugpy/server/inspect/__init__.py @@ -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"" - except: - result = "" - 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) + \ No newline at end of file diff --git a/src/debugpy/server/inspect/children.py b/src/debugpy/server/inspect/children.py new file mode 100644 index 00000000..cc5d68fa --- /dev/null +++ b/src/debugpy/server/inspect/children.py @@ -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) diff --git a/src/debugpy/server/inspect/repr.py b/src/debugpy/server/inspect/repr.py new file mode 100644 index 00000000..3d4b9e9f --- /dev/null +++ b/src/debugpy/server/inspect/repr.py @@ -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"" + except: + result = "" + 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"") + except: + builder.append_text("") + return str(builder) diff --git a/src/debugpy/server/inspect/stdlib.py b/src/debugpy/server/inspect/stdlib.py deleted file mode 100644 index e583b518..00000000 --- a/src/debugpy/server/inspect/stdlib.py +++ /dev/null @@ -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 "}" diff --git a/src/debugpy/server/tracing/__init__.py b/src/debugpy/server/tracing/__init__.py index e140c99e..53a32730 100644 --- a/src/debugpy/server/tracing/__init__.py +++ b/src/debugpy/server/tracing/__init__.py @@ -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 = (