diff --git a/.gitignore b/.gitignore index be955f5..98fae5f 100644 --- a/.gitignore +++ b/.gitignore @@ -80,4 +80,9 @@ typings/ .dynamodb/ dist/ -out/ \ No newline at end of file +out/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class diff --git a/data-extraction/src/CommonDataTypes.ts b/data-extraction/src/CommonDataTypes.ts index cd57eec..e8730c7 100644 --- a/data-extraction/src/CommonDataTypes.ts +++ b/data-extraction/src/CommonDataTypes.ts @@ -228,6 +228,16 @@ export type PlotlyVisualizationData = { text?: string | string[]; xaxis?: string; yaxis?: string; + cells?: { + values?: string[][]; + }; + header?: { + values?: string[]; + }; + domain?: { + x?: number[], + y?: number[], + }; x?: (string | number | null)[] | (string | number | null)[][]; y?: (string | number | null)[] | (string | number | null)[][]; z?: (string | number | null)[] | (string | number | null)[][]; @@ -257,7 +267,8 @@ export type PlotlyVisualizationData = { | "waterfall" | "funnel" | "funnelarea" - | "scattermapbox"; + | "scattermapbox" + | "table"; mode?: | "lines" | "markers" diff --git a/demos/python/Person.py b/demos/python/Person.py new file mode 100644 index 0000000..02277ad --- /dev/null +++ b/demos/python/Person.py @@ -0,0 +1,7 @@ +class Person: + def __init__(self, name, parents=None) -> None: + self.name = name + self.parents = [] if parents is None else parents + + def addParent(self, parent: "Person"): + self.parents.append(parent) diff --git a/demos/python/debugvisualizer.py b/demos/python/debugvisualizer.py new file mode 100644 index 0000000..9ae9426 --- /dev/null +++ b/demos/python/debugvisualizer.py @@ -0,0 +1,44 @@ +from Person import Person +import json +from vscodedebugvisualizer import globalVisualizationFactory + + +class PersonVisualizer: + def checkType(self, t): + return isinstance(t, Person) + + def visualizePerson(self, person: Person, nodes=[], edges=[]): + if person.name in [n["id"] for n in nodes]: + return nodes, edges + + nodes.append( + { + "id": person.name, + "label": person.name, + } + ) + + for p in person.parents: + nodes, edges = self.visualizePerson(p, nodes, edges) + edges.append( + { + "from": p.name, + "to": person.name, + } + ) + + return nodes, edges + + def visualize(self, person: Person): + jsonDict = { + "kind": {"graph": True}, + "nodes": [], + "edges": [], + } + + self.visualizePerson(person, jsonDict["nodes"], jsonDict["edges"]) + + return json.dumps(jsonDict) + + +globalVisualizationFactory.addVisualizer(PersonVisualizer()) diff --git a/demos/python/demo.py b/demos/python/demo.py new file mode 100644 index 0000000..b17ae27 --- /dev/null +++ b/demos/python/demo.py @@ -0,0 +1,123 @@ +import numpy as np + + +# put "x" in the Debug Visualizer and use step by step +# debugging to see the diffrent types of data visualization + + +x = 5 +x = "asdf" +x = 5.5 +x = [1, 2, 3, 4, 5, 6, "a", "b"] +x = ["b", "a", "c", "d", "e"] +x.sort() +x = { + "asdf1": "dasdf", + "asdf2": "dasdf", + "asdf3": {"foo": "bar"}, +} + +x = [1, 2, 3, 4, 5, 6] +# histogram +x = np.concatenate([np.arange(i) for i in range(9)]) +x = x.reshape(2, -1) + + +# one dimension +x = np.arange(100) +x = np.linspace(-np.pi, np.pi, 100) +x = np.sin(x) + +# 2 dimension +x = x.reshape(5, 20) +# 2 dimension transpose +x = x.transpose() +x = x.transpose() + +# 3 dimensions +x = x.reshape(2, 5, 10) + +# 4 dimensions +x = x.reshape(2, 5, 2, 5) + +# big number +x = np.empty(2 ** 24) +x[0:1000000] = 1 +x[1000000:10000000] = 5 + +# pyTorch +try: + import torch + + x = np.concatenate([np.arange(i) for i in range(9)]) + x = x.reshape(2, -1) + x = torch.Tensor(x) + pass + +except ImportError: + pass + + +# tensorflow + +try: + import tensorflow as tf + + x = np.concatenate([np.arange(i) for i in range(9)]) + x = x.reshape(2, -1) + x = tf.convert_to_tensor(x) + pass +except ImportError: + pass + + +# pandas + +try: + import pandas as pd + import plotly.express as px + + x = px.data.gapminder().query("year == 2007") + pass +except ImportError: + pass + +# custom visualizer defined in ./debugvisualizer.py (this file is included automatically) + +from Person import Person + +x = Person("Aria") +parent1 = Person("Eduart") +parent2 = Person("Catelyn") +x.addParent(parent1) +x.addParent(parent2) +parent1.addParent(Person("Benjen")) + +# direct debug-visualizer json as dict with property "kind" + +x = { + "kind": {"dotGraph": True}, + "text": '\ndigraph G {\n subgraph cluster_0 {\n style=filled;\n color=lightgrey;\n node [style=filled,color=white];\n a0 -> a1 -> a2 -> a3;\n label = "process #1";\n }\n \n subgraph cluster_1 {\n node [style=filled];\n b0 -> b1 -> b2 -> b3;\n label = "process #2";\n color=blue\n }\n start -> a0;\n start -> b0;\n a1 -> b3;\n b2 -> a3;\n a3 -> a0;\n a3 -> end;\n b3 -> end;\n \n start [shape=Mdiamond];\n end [shape=Msquare];\n}\n', +} + +x = { + "kind": {"plotly": True}, + "data": [ + { + "mode": "lines", + "type": "scatter", + "x": ["A", "B", "C"], + "xaxis": "x", + "y": [4488916, 3918072, 3892124], + "yaxis": "y", + }, + { + "cells": {"values": [["A", "B", "C"], [341319, 281489, 294786], [4488916, 3918072, 3892124]]}, + "domain": {"x": [0.0, 1.0], "y": [0.0, 0.60]}, + "header": {"align": "left", "font": {"size": 10}, "values": ["Date", "Number", "Output"]}, + "type": "table", + }, + ], + "layout": {"xaxis": {"anchor": "y", "domain": [0.0, 1.0]}, "yaxis": {"anchor": "x", "domain": [0.75, 1.0]}}, +} +pass \ No newline at end of file diff --git a/demos/python/graph.py b/demos/python/graph.py index 6a178df..9e631d2 100644 --- a/demos/python/graph.py +++ b/demos/python/graph.py @@ -23,10 +23,9 @@ for i in range(2,100): # connects the node to a random edge targetId = str(randint(1, i - 1)) graph["edges"].append({"from": id, "to": targetId}) - json_graph = dumps(graph) print("i is " + str(i)) # try setting a breakpoint right above - # then put json_graph into the visualization console and press enter + # then put graph into the visualization console and press enter # when you step through the code each time you hit the breakpoint # the graph should automatically refresh! diff --git a/demos/python/insertion_sort.py b/demos/python/insertion_sort.py index 4bba40b..e5a4ce0 100644 --- a/demos/python/insertion_sort.py +++ b/demos/python/insertion_sort.py @@ -1,5 +1,4 @@ """Python demo for sorting using VS Code Debug Visualizer.""" -import json def serialize(arr): @@ -14,7 +13,7 @@ def serialize(arr): } ], } - return json.dumps(formatted) + return formatted arr = [6, 9, 3, 12, 1, 11, 5, 13, 8, 14, 2, 4, 10, 0, 7] diff --git a/extension/src/VisualizationBackend/PyVisualizationSupport.ts b/extension/src/VisualizationBackend/PyVisualizationSupport.ts new file mode 100644 index 0000000..d043af4 --- /dev/null +++ b/extension/src/VisualizationBackend/PyVisualizationSupport.ts @@ -0,0 +1,151 @@ +import { + DataExtractionResult, + DataExtractorId, + GraphNode, + GraphVisualizationData, +} from "@hediet/debug-visualizer-data-extraction"; +import { DebugSessionProxy } from "../proxies/DebugSessionProxy"; +import { + DebugSessionVisualizationSupport, + VisualizationBackend, + GetVisualizationDataArgs, + VisualizationBackendBase, +} from "./VisualizationBackend"; +import { Config } from "../Config"; +import { parseEvaluationResultFromGenericDebugAdapter } from "./parseEvaluationResultFromGenericDebugAdapter"; +import { FormattedMessage } from "../webviewContract"; +import { hotClass, registerUpdateReconciler } from "@hediet/node-reload"; +import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy"; + +registerUpdateReconciler(module); + +@hotClass(module) +export class PyEvaluationEngine + implements DebugSessionVisualizationSupport { + constructor( + private readonly debuggerView: DebuggerViewProxy, + private readonly config: Config + ) { } + + createBackend( + session: DebugSessionProxy + ): VisualizationBackend | undefined { + + const supportedDebugAdapters = [ + "python", + ]; + + if (supportedDebugAdapters.indexOf(session.session.type) !== -1) { + return new PyVisualizationBackend( + session, + this.debuggerView, + this.config + ); + } + return undefined; + } +} + +export class PyVisualizationBackend extends VisualizationBackendBase { + public readonly expressionLanguageId = "python"; + + constructor( + debugSession: DebugSessionProxy, + debuggerView: DebuggerViewProxy, + private readonly config: Config + ) { + super(debugSession, debuggerView); + } + + protected getContext(): "watch" | "repl" { + // we will use "repl" as default so that results are not truncated. + return "repl"; + } + + public async getVisualizationData({ + expression, + preferredExtractorId, + }: GetVisualizationDataArgs): Promise< + | { kind: "data"; result: DataExtractionResult } + | { kind: "error"; message: FormattedMessage } + > { + const frameId = this.debuggerView.getActiveStackFrameId( + this.debugSession + ); + + const finalExpression = this.getFinalExpression({ + expression, + preferredExtractorId, + }); + let reply: { result: string; variablesReference: number }; + try { + // inject vscodedebugvisualizer for python + await this.debugSession.evaluate({ + expression: 'from vscodedebugvisualizer import visualize\ntry:\n import debugvisualizer\nexcept ImportError:\n pass', + frameId, + context: this.getContext(), + }); + + reply = await this.debugSession.evaluate({ + expression: finalExpression, + frameId, + context: this.getContext(), + }); + + let result = reply.result; + // remove the initial escape by the the debug session e.g. `''{"kind": {"text": true}, "text": "{"asdf1\'"}''` + result = result.replaceAll(/\\'/g, "'"); + result = result.replaceAll(/\\'/g, "'"); + result = result.replaceAll(/\\\\/g, "\\") + result.substr(1, result.length - 2); + + return parseEvaluationResultFromGenericDebugAdapter( + result, + { + debugAdapterType: this.debugSession.session + .configuration.type, + } + ); + } catch (error) { + let errorTyped = error as Error; + if (errorTyped.message.includes("ModuleNotFoundError: No module named 'vscodedebugvisualizer'")) { + return { + kind: "error", + message: { + kind: "list", + items: ["Pleas make sure vscodedebugvisualizer is installed: `pip install vscodedebugvisualizer`"], + }, + }; + } + return { + kind: "error", + message: { + kind: "list", + items: [ + "An error occurred while evaluating the expression:", + errorTyped.message, + `Used debug adapter: ${this.debugSession.session.configuration.type}`, + { + kind: "inlineList", + items: [ + "Evaluated expression is", + { kind: "code", content: finalExpression }, + ], + }, + ], + }, + }; + } + } + + protected getFinalExpression(args: { + expression: string; + preferredExtractorId: DataExtractorId | undefined; + }): string { + // wrap expression with visualize function + let pythonInject = ""; + pythonInject += "visualize(" + args.expression + ")"; + return pythonInject; + } + +} diff --git a/extension/src/VisualizationBackend/index.ts b/extension/src/VisualizationBackend/index.ts index 0f7ae00..2afe268 100644 --- a/extension/src/VisualizationBackend/index.ts +++ b/extension/src/VisualizationBackend/index.ts @@ -2,5 +2,6 @@ export * from "./VisualizationBackend"; export { ComposedVisualizationSupport } from "./ComposedVisualizationSupport"; export { JsEvaluationEngine } from "./JsVisualizationSupport"; +export { PyEvaluationEngine } from "./PyVisualizationSupport"; export { GenericVisualizationSupport } from "./GenericVisualizationSupport"; export { ConfigurableVisualizationSupport } from "./ConfigurableVisualizationSupport"; diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 0faaeb2..b577b1e 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -21,6 +21,7 @@ import { VisualizationWatchModelImpl } from "./VisualizationWatchModel"; import { ComposedVisualizationSupport, JsEvaluationEngine, + PyEvaluationEngine, GenericVisualizationSupport, ConfigurableVisualizationSupport, } from "./VisualizationBackend"; @@ -32,7 +33,7 @@ export function activate(context: ExtensionContext) { ); } -export function deactivate() {} +export function deactivate() { } export class Extension { public readonly dispose = Disposable.fn(); @@ -51,6 +52,7 @@ export class Extension { this.debuggerView ), new JsEvaluationEngine(this.debuggerView, this.config), + new PyEvaluationEngine(this.debuggerView, this.config), new GenericVisualizationSupport(this.debuggerView), ]), this.debuggerView