Skip to content

Commit

Permalink
Refactor evaluation logic to have a separate DAP-unaware object inspe…
Browse files Browse the repository at this point in the history
…ction layer.
  • Loading branch information
Pavel Minaev authored and int19h committed Dec 13, 2023
1 parent 6b5806c commit a15a1ce
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 129 deletions.
72 changes: 41 additions & 31 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ exclude = '''
'''

[tool.pyright]
pythonVersion = "3.8"
include = ["src/**", "tests/**" ]
pythonVersion = "3.12"
include = ["src/**", "tests/**"]
extraPaths = ["src/debugpy/_vendored/pydevd"]
ignore = ["src/debugpy/_vendored/pydevd", "src/debugpy/_version.py"]
executionEnvironments = [
{ root = "src" }, { root = "." }
{ root = "src" },
{ root = "." },
]

[tool.ruff]
Expand All @@ -28,10 +29,19 @@ executionEnvironments = [
# McCabe complexity (`C901`) by default.
select = ["E", "F"]
ignore = [
"E203", "E221", "E222", "E226", "E261", "E262", "E265", "E266",
"E401", "E402",
"E501",
"E722", "E731"
"E203",
"E221",
"E222",
"E226",
"E261",
"E262",
"E265",
"E266",
"E401",
"E402",
"E501",
"E722",
"E731",
]

# Allow autofix for all enabled rules (when `--fix`) is provided.
Expand All @@ -40,29 +50,29 @@ unfixable = []

# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
"versioneer.py",
"src/debugpy/_vendored/pydevd"
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
"versioneer.py",
"src/debugpy/_vendored/pydevd",
]
per-file-ignores = {}

Expand All @@ -73,4 +83,4 @@ line-length = 88
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

# Assume Python 3.8
target-version = "py38"
target-version = "py38"
2 changes: 1 addition & 1 deletion src/debugpy/server/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from debugpy.server.tracing import Breakpoint, StackFrame


class Adapter(object):
class Adapter:
"""Represents the debug adapter connected to this debug server."""

class Capabilities(components.Capabilities):
Expand Down
111 changes: 14 additions & 97 deletions src/debugpy/server/eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@

import threading

from collections.abc import Iterable, Mapping
from itertools import count
from collections.abc import Iterable
from types import FrameType
from typing import ClassVar, Dict, Literal, Self

from debugpy.server import tracing
from debugpy.common import log
from debugpy.server.safe_repr import SafeRepr
from debugpy.server.inspect import inspect

ScopeKind = Literal["global", "nonlocal", "local"]

Expand Down Expand Up @@ -99,28 +97,6 @@ def __init__(self, frame: "tracing.StackFrame", name: str, value: object):
self.name = name
self.value = value

if isinstance(value, Mapping):
self._items = self._items_dict
else:
try:
it = iter(value)
except:
it = None
# Use (iter(value) is value) to distinguish iterables from iterators.
if it is not None and it is not value:
self._items = self._items_iterable

@property
def typename(self) -> str:
try:
return type(self.value).__name__
except:
return ""

@property
def repr(self) -> str:
return SafeRepr()(self.value)

def __getstate__(self):
state = super().__getstate__()
state.update(
Expand All @@ -132,82 +108,23 @@ def __getstate__(self):
)
return state

def variables(self) -> Iterable["Variable"]:
get_name = lambda var: var.name
return [
*sorted(self._attributes(), key=get_name),
*sorted(self._synthetic(), key=get_name),
*self._items(),
]

def _attributes(self) -> Iterable["Variable"]:
# TODO: group class/instance/function/special
@property
def typename(self) -> str:
try:
names = dir(self.value)
return type(self.value).__name__
except:
names = []
for name in names:
if name.startswith("__"):
continue
try:
value = getattr(self.value, name)
except BaseException as exc:
value = exc
try:
if hasattr(type(value), "__call__"):
continue
except:
pass
yield Variable(self.frame, name, value)
return ""

def _synthetic(self) -> Iterable["Variable"]:
try:
length = len(self.value)
except:
pass
else:
yield Variable(self.frame, "len()", length)
@property
def repr(self) -> str:
return "".join(inspect(self.value).repr())

def _items(self) -> Iterable["Variable"]:
return ()
def variables(self) -> Iterable["Variable"]:
for child in inspect(self.value).children():
yield Variable(self.frame, child.name, child.value)

def _items_iterable(self) -> Iterable["Variable"]:
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 Variable(self.frame, f"[{i}]", item)

def _items_dict(self) -> Iterable["Variable"]:
try:
keys = self.value.keys()
except:
return
it = iter(keys)
safe_repr = SafeRepr()
while True:
try:
key = next(it)
except StopIteration:
break
except:
break
try:
value = self.value[key]
except BaseException as exc:
value = exc
yield Variable(self.frame, f"[{safe_repr(key)}]", value)


def evaluate(expr: str, frame_id: int):

def evaluate(expr: str, frame_id: int) -> Variable:
from debugpy.server.tracing import StackFrame

frame = StackFrame.get(frame_id)
Expand Down
87 changes: 87 additions & 0 deletions src/debugpy/server/inspect/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.

"""
Object inspection: rendering values, enumerating children etc.
"""

from typing import Iterable


class ChildObject:
name: str
value: object

def __init__(self, value: object):
self.value = value

@property
def name(self) -> str:
raise NotImplementedError

def expr(self, obj: object) -> str:
raise NotImplementedError


class ChildAttribute(ChildObject):
name: str

def __init__(self, name: str, value: object):
super().__init__(value)
self.name = name

def expr(self, obj_expr: str) -> str:
return f"({obj_expr}).{self.name}"


class ObjectInspector:
"""
Inspects a generic object. Uses builtins.repr() to render values and dir() to enumerate children.
"""

obj: object

def __init__(self, obj: object):
self.obj = obj

def repr(self) -> Iterable[str]:
yield repr(self.obj)

def children(self) -> Iterable[ChildObject]:
return sorted(self._attributes(), key=lambda var: var.name)

def _attributes(self) -> Iterable[ChildObject]:
# TODO: group class/instance/function/special
try:
names = dir(self.obj)
except:
names = []
for name in names:
if name.startswith("__"):
continue
try:
value = getattr(self.obj, name)
except BaseException as exc:
value = exc
try:
if hasattr(type(value), "__call__"):
continue
except:
pass
yield ChildAttribute(name, value)


def inspect(obj: object) -> ObjectInspector:
from debugpy.server.inspect import stdlib

# TODO: proper extensible registry
match obj:
case list():
return stdlib.ListInspector(obj)
case {}:
return stdlib.MappingInspector(obj)
case []:
return stdlib.SequenceInspector(obj)
case _:
return ObjectInspector(obj)
Loading

0 comments on commit a15a1ce

Please sign in to comment.