From ae0f8be63a9a0d7ec69d3ef4da8384b5ac2b1300 Mon Sep 17 00:00:00 2001 From: Pavel Minaev Date: Wed, 20 Mar 2024 23:06:19 -0700 Subject: [PATCH] Implement #1441: Editing variable values Implement #1442: Expression variables --- src/debugpy/server/adapters.py | 40 ++++++++- src/debugpy/server/eval.py | 108 +++++++++++++------------ src/debugpy/server/inspect/__init__.py | 6 +- src/debugpy/server/tracing/__init__.py | 8 +- 4 files changed, 99 insertions(+), 63 deletions(-) diff --git a/src/debugpy/server/adapters.py b/src/debugpy/server/adapters.py index d3a70eed..0f6ee39a 100644 --- a/src/debugpy/server/adapters.py +++ b/src/debugpy/server/adapters.py @@ -421,10 +421,42 @@ class Adapter: def evaluate_request(self, request: Request): expr = request("expression", str) - frameId = request("frameId", int) - var = eval.evaluate(expr, frameId) - return {"result": var.repr, "variablesReference": var.id} - + frame_id = request("frameId", int) + frame = StackFrame.get(frame_id) + if frame is None: + return request.isnt_valid(f'Invalid "frameId": {frame_id}', silent=True) + try: + result = frame.evaluate(expr) + except BaseException as exc: + result = exc + return eval.Result(frame, result) + + def setVariable_request(self, request: Request): + name = request("name", str) + value = request("value", str) + container_id = request("variablesReference", int) + container = eval.VariableContainer.get(container_id) + if container is None: + raise request.isnt_valid(f'Invalid "variablesReference": {container_id}') + try: + return container.set_variable(name, value) + except BaseException as exc: + raise request.cant_handle(str(exc)) + + def setExpression_request(self, request: Request): + expr = request("expression", str) + value = request("value", str) + frame_id = request("frameId", int) + frame = StackFrame.get(frame_id) + if frame is None: + return request.isnt_valid(f'Invalid "frameId": {frame_id}', silent=True) + try: + frame.evaluate(f"{expr} = ({value})", "exec") + result = frame.evaluate(expr) + except BaseException as exc: + raise request.cant_handle(str(exc)) + return eval.Result(frame, result) + def disconnect_request(self, request: Request): Breakpoint.clear() self._tracer.abandon_step() diff --git a/src/debugpy/server/eval.py b/src/debugpy/server/eval.py index cfa1e8db..7b301bf1 100644 --- a/src/debugpy/server/eval.py +++ b/src/debugpy/server/eval.py @@ -7,9 +7,8 @@ import threading from collections.abc import Iterable from debugpy.server.inspect import inspect from types import FrameType -from typing import ClassVar, Dict, Literal, Self +from typing import ClassVar, Dict, Self -type ScopeKind = Literal["global", "nonlocal", "local"] type StackFrame = "debugpy.server.tracing.StackFrame" @@ -30,7 +29,7 @@ class VariableContainer: self.id = VariableContainer._last_id self._all[self.id] = self - def __getstate__(self): + def __getstate__(self) -> dict[str, object]: return {"variablesReference": self.id} def __repr__(self): @@ -44,6 +43,9 @@ class VariableContainer: def variables(self) -> Iterable["Variable"]: raise NotImplementedError + def set_variable(self, name: str, value: str) -> "Value": + raise NotImplementedError + @classmethod def invalidate(self, *frames: Iterable[StackFrame]) -> None: with _lock: @@ -56,51 +58,18 @@ class VariableContainer: del self._all[id] -class Scope(VariableContainer): - frame: FrameType - kind: ScopeKind - - def __init__(self, frame: StackFrame, kind: ScopeKind): - super().__init__(frame) - self.kind = kind - - def __getstate__(self): - state = super().__getstate__() - state.update( - { - "name": self.kind, - "presentationHint": self.kind, - } - ) - return state - - def variables(self) -> Iterable["Variable"]: - match self.kind: - case "global": - d = self.frame.f_globals - case "local": - d = self.frame.f_locals - case _: - raise ValueError(f"Unknown scope kind: {self.kind}") - for name, value in d.items(): - yield Variable(self.frame, name, value) - - -class Variable(VariableContainer): - name: str +class Value(VariableContainer): value: object - # TODO: evaluateName, memoryReference, presentationHint + # TODO: memoryReference, presentationHint - def __init__(self, frame: StackFrame, name: str, value: object): + def __init__(self, frame: StackFrame, value: object): super().__init__(frame) - self.name = name self.value = value - def __getstate__(self): + def __getstate__(self) -> dict[str, object]: state = super().__getstate__() state.update( { - "name": self.name, "value": self.repr, "type": self.typename, } @@ -122,17 +91,52 @@ class Variable(VariableContainer): for child in inspect(self.value).children(): yield Variable(self.frame, child.name, child.value) + def set_variable(self, name: str, value_expr: str) -> "Value": + value = self.frame.evaluate(value_expr) + if name.startswith("[") and name.endswith("]"): + key_expr = name[1:-1] + key = self.frame.evaluate(key_expr) + self.value[key] = value + result = self.value[key] + else: + setattr(self.value, name, value) + result = getattr(self.value, name) + return Value(self.frame, result) + -def evaluate(expr: str, frame_id: int) -> Variable: - from debugpy.server.tracing import StackFrame +class Result(Value): + def __getstate__(self) -> dict[str, object]: + state = super().__getstate__() + state["result"] = state.pop("value") + return state - frame = StackFrame.get(frame_id) - if frame is None: - raise ValueError(f"Invalid frame ID: {frame_id}") - fobj = frame.frame_object - try: - code = compile(expr, "", "eval") - result = eval(code, fobj.f_globals, fobj.f_locals) - except BaseException as exc: - result = exc - return Variable(frame, expr, result) + +class Variable(Value): + name: str + # TODO: evaluateName + + def __init__(self, frame: StackFrame, name: str, value: object): + super().__init__(frame, value) + self.name = name + + def __getstate__(self) -> dict[str, object]: + state = super().__getstate__() + state["name"] = self.name + return state + + +class Scope(Variable): + frame: FrameType + + def __init__(self, frame: StackFrame, name: str, storage: dict[str, object]): + class ScopeObject: + def __dir__(self): + return list(storage.keys()) + + def __getattr__(self, name): + return storage[name] + + def __setattr__(self, name, value): + storage[name] = value + + super().__init__(frame, name, ScopeObject()) diff --git a/src/debugpy/server/inspect/__init__.py b/src/debugpy/server/inspect/__init__.py index 4e1d8a96..fed29fb5 100644 --- a/src/debugpy/server/inspect/__init__.py +++ b/src/debugpy/server/inspect/__init__.py @@ -16,10 +16,6 @@ class ChildObject: def __init__(self, value: object): self.value = value - @property - def name(self) -> str: - raise NotImplementedError - def expr(self, obj: object) -> str: raise NotImplementedError @@ -72,7 +68,7 @@ class ObjectInspector: except BaseException as exc: value = exc try: - if hasattr(type(value), "__call__"): + if hasattr(value, "__call__"): continue except: pass diff --git a/src/debugpy/server/tracing/__init__.py b/src/debugpy/server/tracing/__init__.py index 94c18197..32d008c9 100644 --- a/src/debugpy/server/tracing/__init__.py +++ b/src/debugpy/server/tracing/__init__.py @@ -339,10 +339,14 @@ class StackFrame: def scopes(self) -> List[Scope]: if self._scopes is None: self._scopes = [ - Scope(self.frame_object, "local"), - Scope(self.frame_object, "global"), + Scope(self, "local", self.frame_object.f_locals), + Scope(self, "global", self.frame_object.f_globals), ] return self._scopes + + def evaluate(self, source: str, mode: Literal["eval", "exec", "single"] = "eval") -> object: + code = compile(source, "", mode) + return eval(code, self.frame_object.f_globals, self.frame_object.f_locals) @classmethod def invalidate(self, thread: Thread):