From 0258f4b90cc5089488c5d0c862218d9800422dc4 Mon Sep 17 00:00:00 2001 From: amunger Date: Tue, 10 Sep 2024 16:11:36 -0700 Subject: [PATCH 01/23] much code --- package.json | 3 +- python_files/get_variable_info.py | 594 ++++++++++++++++++ src/client/repl/nativeRepl.ts | 6 +- src/client/repl/pythonServer.ts | 2 +- src/client/repl/variables/types.ts | 17 + .../repl/variables/variableResultCache.ts | 33 + .../repl/variables/variablesProvider.ts | 172 +++++ ...ode.proposed.notebookVariableProvider.d.ts | 55 ++ 8 files changed, 878 insertions(+), 4 deletions(-) create mode 100644 python_files/get_variable_info.py create mode 100644 src/client/repl/variables/types.ts create mode 100644 src/client/repl/variables/variableResultCache.ts create mode 100644 src/client/repl/variables/variablesProvider.ts create mode 100644 types/vscode.proposed.notebookVariableProvider.d.ts diff --git a/package.json b/package.json index 3cd9b017532b..ba8fca9991b1 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "quickPickItemTooltip", "terminalDataWriteEvent", "terminalExecuteCommandEvent", - "contribIssueReporter" + "contribIssueReporter", + "notebookVariableProvider" ], "author": { "name": "Microsoft Corporation" diff --git a/python_files/get_variable_info.py b/python_files/get_variable_info.py new file mode 100644 index 000000000000..8ee8cf30371d --- /dev/null +++ b/python_files/get_variable_info.py @@ -0,0 +1,594 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root +# for license information. + +# Gotten from ptvsd for supporting the format expected there. +import json +import locale +import sys +from importlib.util import find_spec +from typing import NamedTuple + + +# The pydevd SafeRepr class used in ptvsd/debugpy +class SafeRepr(object): # noqa: UP004 + # Can be used to override the encoding from locale.getpreferredencoding() + locale_preferred_encoding = None + + # Can be used to override the encoding used for sys.stdout.encoding + sys_stdout_encoding = None + + # String types are truncated to maxstring_outer when at the outer- + # most level, and truncated to maxstring_inner characters inside + # collections. + maxstring_outer = 2**16 + maxstring_inner = 128 + string_types = (str, bytes) + bytes = bytes + set_info = (set, "{", "}", False) + frozenset_info = (frozenset, "frozenset({", "})", False) + int_types = (int,) + long_iter_types = (list, tuple, bytearray, range, dict, set, frozenset) + + # Collection types are recursively iterated for each limit in + # maxcollection. + maxcollection = (60, 20) + + # Specifies type, prefix string, suffix string, and whether to include a + # comma if there is only one element. (Using a sequence rather than a + # mapping because we use isinstance() to determine the matching type.) + collection_types = [ # noqa: RUF012 + (tuple, "(", ")", True), + (list, "[", "]", False), + frozenset_info, + set_info, + ] + try: + from collections import deque + + collection_types.append((deque, "deque([", "])", False)) + except Exception: + pass + + # type, prefix string, suffix string, item prefix string, + # item key/value separator, item suffix string + dict_types = [(dict, "{", "}", "", ": ", "")] # noqa: RUF012 + try: + from collections import OrderedDict + + dict_types.append((OrderedDict, "OrderedDict([", "])", "(", ", ", ")")) + except Exception: + pass + + # All other types are treated identically to strings, but using + # different limits. + maxother_outer = 2**16 + maxother_inner = 128 + + convert_to_hex = False + raw_value = False + + def __call__(self, obj): + """ + :param object obj: + The object for which we want a representation. + + :return str: + Returns bytes encoded as utf-8 on py2 and str on py3. + """ # noqa: D205 + try: + return "".join(self._repr(obj, 0)) + except Exception: + try: + return f"An exception was raised: {sys.exc_info()[1]!r}" + except Exception: + return "An exception was raised" + + def _repr(self, obj, level): + """Returns an iterable of the parts in the final repr string.""" + try: + obj_repr = type(obj).__repr__ + except Exception: + obj_repr = None + + def has_obj_repr(t): + r = t.__repr__ + try: + return obj_repr == r + except Exception: + return obj_repr is r + + for t, prefix, suffix, comma in self.collection_types: + if isinstance(obj, t) and has_obj_repr(t): + return self._repr_iter(obj, level, prefix, suffix, comma) + + for ( + t, + prefix, + suffix, + item_prefix, + item_sep, + item_suffix, + ) in self.dict_types: + if isinstance(obj, t) and has_obj_repr(t): + return self._repr_dict( + obj, level, prefix, suffix, item_prefix, item_sep, item_suffix + ) + + for t in self.string_types: + if isinstance(obj, t) and has_obj_repr(t): + return self._repr_str(obj, level) + + if self._is_long_iter(obj): + return self._repr_long_iter(obj) + + return self._repr_other(obj, level) + + # Determines whether an iterable exceeds the limits set in + # maxlimits, and is therefore unsafe to repr(). + def _is_long_iter(self, obj, level=0): + try: + # Strings have their own limits (and do not nest). Because + # they don't have __iter__ in 2.x, this check goes before + # the next one. + if isinstance(obj, self.string_types): + return len(obj) > self.maxstring_inner + + # If it's not an iterable (and not a string), it's fine. + if not hasattr(obj, "__iter__"): + return False + + # If it's not an instance of these collection types then it + # is fine. Note: this is a fix for + # https://github.com/Microsoft/ptvsd/issues/406 + if not isinstance(obj, self.long_iter_types): + return False + + # Iterable is its own iterator - this is a one-off iterable + # like generator or enumerate(). We can't really count that, + # but repr() for these should not include any elements anyway, + # so we can treat it the same as non-iterables. + if obj is iter(obj): + return False + + # range reprs fine regardless of length. + if isinstance(obj, range): + return False + + # numpy and scipy collections (ndarray etc) have + # self-truncating repr, so they're always safe. + try: + module = type(obj).__module__.partition(".")[0] + if module in ("numpy", "scipy"): + return False + except Exception: + pass + + # Iterables that nest too deep are considered long. + if level >= len(self.maxcollection): + return True + + # It is too long if the length exceeds the limit, or any + # of its elements are long iterables. + if hasattr(obj, "__len__"): + try: + size = len(obj) + except Exception: + size = None + if size is not None and size > self.maxcollection[level]: + return True + return any( + self._is_long_iter(item, level + 1) for item in obj + ) + return any( + i > self.maxcollection[level] or self._is_long_iter(item, level + 1) + for i, item in enumerate(obj) + ) + + except Exception: + # If anything breaks, assume the worst case. + return True + + def _repr_iter(self, obj, level, prefix, suffix, comma_after_single_element=False): # noqa: FBT002 + yield prefix + + if level >= len(self.maxcollection): + yield "..." + else: + count = self.maxcollection[level] + yield_comma = False + for item in obj: + if yield_comma: + yield ", " + yield_comma = True + + count -= 1 + if count <= 0: + yield "..." + break + + yield from self._repr(item, 100 if item is obj else level + 1) + else: + if comma_after_single_element: # noqa: SIM102 + if count == self.maxcollection[level] - 1: + yield "," + yield suffix + + def _repr_long_iter(self, obj): + try: + length = hex(len(obj)) if self.convert_to_hex else len(obj) + obj_repr = f"<{type(obj).__name__}, len() = {length}>" + except Exception: + try: + obj_repr = "<" + type(obj).__name__ + ">" + except Exception: + obj_repr = "" + yield obj_repr + + def _repr_dict( + self, obj, level, prefix, suffix, item_prefix, item_sep, item_suffix + ): + if not obj: + yield prefix + suffix + return + if level >= len(self.maxcollection): + yield prefix + "..." + suffix + return + + yield prefix + + count = self.maxcollection[level] + yield_comma = False + + obj_keys = list(obj) + + for key in obj_keys: + if yield_comma: + yield ", " + yield_comma = True + + count -= 1 + if count <= 0: + yield "..." + break + + yield item_prefix + for p in self._repr(key, level + 1): + yield p + + yield item_sep + + try: + item = obj[key] + except Exception: + yield "" + else: + for p in self._repr(item, 100 if item is obj else level + 1): + yield p + yield item_suffix + + yield suffix + + def _repr_str(self, obj, level): + try: + if self.raw_value: + # For raw value retrieval, ignore all limits. + if isinstance(obj, bytes): + yield obj.decode("latin-1") + else: + yield obj + return + + limit_inner = self.maxother_inner + limit_outer = self.maxother_outer + limit = limit_inner if level > 0 else limit_outer + if len(obj) <= limit: + # Note that we check the limit before doing the repr (so, the final string + # may actually be considerably bigger on some cases, as besides + # the additional u, b, ' chars, some chars may be escaped in repr, so + # even a single char such as \U0010ffff may end up adding more + # chars than expected). + yield self._convert_to_unicode_or_bytes_repr(repr(obj)) + return + + # Slightly imprecise calculations - we may end up with a string that is + # up to 6 characters longer than limit. If you need precise formatting, + # you are using the wrong class. + left_count, right_count = max(1, int(2 * limit / 3)), max( + 1, int(limit / 3) + ) + + # Important: only do repr after slicing to avoid duplicating a byte array that could be + # huge. + + # Note: we don't deal with high surrogates here because we're not dealing with the + # repr() of a random object. + # i.e.: A high surrogate unicode char may be splitted on Py2, but as we do a `repr` + # afterwards, that's ok. + + # Also, we just show the unicode/string/bytes repr() directly to make clear what the + # input type was (so, on py2 a unicode would start with u' and on py3 a bytes would + # start with b'). + + part1 = obj[:left_count] + part1 = repr(part1) + part1 = part1[: part1.rindex("'")] # Remove the last ' + + part2 = obj[-right_count:] + part2 = repr(part2) + part2 = part2[ + part2.index("'") + 1 : + ] # Remove the first ' (and possibly u or b). + + yield part1 + yield "..." + yield part2 + except: # noqa: E722 + # This shouldn't really happen, but let's play it safe. + # exception('Error getting string representation to show.') + yield from self._repr_obj( + obj, level, self.maxother_inner, self.maxother_outer + ) + + def _repr_other(self, obj, level): + return self._repr_obj(obj, level, self.maxother_inner, self.maxother_outer) + + def _repr_obj(self, obj, level, limit_inner, limit_outer): + try: + if self.raw_value: + # For raw value retrieval, ignore all limits. + if isinstance(obj, bytes): + yield obj.decode("latin-1") + return + + try: + mv = memoryview(obj) + except Exception: + yield self._convert_to_unicode_or_bytes_repr(repr(obj)) + return + else: + # Map bytes to Unicode codepoints with same values. + yield mv.tobytes().decode("latin-1") + return + elif self.convert_to_hex and isinstance(obj, self.int_types): + obj_repr = hex(obj) + else: + obj_repr = repr(obj) + except Exception: + try: + obj_repr = object.__repr__(obj) + except Exception: + try: + obj_repr = ( + "" + ) + except Exception: + obj_repr = "" + + limit = limit_inner if level > 0 else limit_outer + + if limit >= len(obj_repr): + yield self._convert_to_unicode_or_bytes_repr(obj_repr) + return + + # Slightly imprecise calculations - we may end up with a string that is + # up to 3 characters longer than limit. If you need precise formatting, + # you are using the wrong class. + left_count, right_count = max(1, int(2 * limit / 3)), max( + 1, int(limit / 3) + ) + + yield obj_repr[:left_count] + yield "..." + yield obj_repr[-right_count:] + + def _convert_to_unicode_or_bytes_repr(self, obj_repr): + return obj_repr + + def _bytes_as_unicode_if_possible(self, obj_repr): + # We try to decode with 3 possible encoding (sys.stdout.encoding, + # locale.getpreferredencoding() and 'utf-8). If no encoding can decode + # the input, we return the original bytes. + try_encodings = [] + encoding = self.sys_stdout_encoding or getattr(sys.stdout, "encoding", "") + if encoding: + try_encodings.append(encoding.lower()) + + preferred_encoding = ( + self.locale_preferred_encoding or locale.getpreferredencoding() + ) + if preferred_encoding: + preferred_encoding = preferred_encoding.lower() + if preferred_encoding not in try_encodings: + try_encodings.append(preferred_encoding) + + if "utf-8" not in try_encodings: + try_encodings.append("utf-8") + + for encoding in try_encodings: + try: + return obj_repr.decode(encoding) + except UnicodeDecodeError: # noqa: PERF203 + pass + + return obj_repr # Return the original version (in bytes) + + +safe_repr = SafeRepr() +collection_types = ["list", "tuple", "set"] +array_page_size = 50 + +DisplayOptions = NamedTuple("DisplayOptions", ["width", "max_columns"]) + + +def set_pandas_display_options(display_options=None): + if find_spec("pandas") is not None: + try: + import pandas as _VSCODE_PD # type: ignore # noqa: N812 + + original_display = DisplayOptions( + width=_VSCODE_PD.options.display.width, + max_columns=_VSCODE_PD.options.display.max_columns, + ) + + if display_options: + _VSCODE_PD.options.display.max_columns = display_options.max_columns + _VSCODE_PD.options.display.width = display_options.width + else: + _VSCODE_PD.options.display.max_columns = 100 + _VSCODE_PD.options.display.width = 1000 + + return original_display + except ImportError: + pass + finally: + del _VSCODE_PD + return None + + +def get_value(variable): + original_display = None + if type(variable).__name__ == "DataFrame" and find_spec("pandas") is not None: + original_display = set_pandas_display_options() + + try: + return safe_repr(variable) + finally: + if original_display: + set_pandas_display_options(original_display) + + +def get_property_names(variable): + props = [] + private_props = [] + for prop in dir(variable): + if not prop.startswith("_"): + props.append(prop) + elif not prop.startswith("__"): + private_props.append(prop) + return props + private_props + + +def get_full_type(var_type): + module = "" + if hasattr(var_type, "__module__") and var_type.__module__ != "builtins": + module = var_type.__module__ + "." + if hasattr(var_type, "__qualname__"): + return module + var_type.__qualname__ + elif hasattr(var_type, "__name__"): + return module + var_type.__name__ + return None + + +types_to_exclude = ["module", "function", "method", "class", "type"] + + +def get_variable_description(variable): + result = {} + + var_type = type(variable) + result["type"] = get_full_type(var_type) + if hasattr(var_type, "__mro__"): + result["interfaces"] = [get_full_type(t) for t in var_type.__mro__] + + if hasattr(variable, "__len__") and result["type"] in collection_types: + result["count"] = len(variable) + + result["hasNamedChildren"] = hasattr(variable, "__dict__") or isinstance( + variable, dict + ) + + result["value"] = get_value(variable) + return result + + +def get_child_property(root, property_chain): + try: + variable = root + for prop in property_chain: + if isinstance(prop, int): + if hasattr(variable, "__getitem__"): + variable = variable[prop] + elif isinstance(variable, set): + variable = list(variable)[prop] + else: + return None + elif hasattr(variable, prop): + variable = getattr(variable, prop) + elif isinstance(variable, dict) and prop in variable: + variable = variable[prop] + else: + return None + except Exception: + return None + + return variable + + +### Get info on variables at the root level +def _VSCODE_getVariableDescriptions(var_names): # noqa: N802 + variables = [ + { + "name": varName, + **get_variable_description(globals()[varName]), + "root": varName, + "propertyChain": [], + "language": "python", + } + for varName in var_names + if varName in globals() + and type(globals()[varName]).__name__ not in types_to_exclude + ] + + return json.dumps(variables) + + +### Get info on children of a variable reached through the given property chain +def _VSCODE_getAllChildrenDescriptions(root_var_name, property_chain, start_index): # noqa: N802 + root = globals()[root_var_name] + if root is None: + return [] + + parent = root + if len(property_chain) > 0: + parent = get_child_property(root, property_chain) + + children = [] + parent_info = get_variable_description(parent) + if "count" in parent_info: + if parent_info["count"] > 0: + last_item = min(parent_info["count"], start_index + array_page_size) + index_range = range(start_index, last_item) + children = [ + { + **get_variable_description(get_child_property(parent, [i])), + "name": str(i), + "root": root_var_name, + "propertyChain": [*property_chain, i], + "language": "python", + } + for i in index_range + ] + elif parent_info["hasNamedChildren"]: + children_names = [] + if hasattr(parent, "__dict__"): + children_names = get_property_names(parent) + elif isinstance(parent, dict): + children_names = list(parent.keys()) + + children = [] + for prop in children_names: + child_property = get_child_property(parent, [prop]) + if ( + child_property is not None + and type(child_property).__name__ not in types_to_exclude + ): + child = { + **get_variable_description(child_property), + "name": prop, + "root": root_var_name, + "propertyChain": [*property_chain, prop], + } + children.append(child) + + return json.dumps(children) diff --git a/src/client/repl/nativeRepl.ts b/src/client/repl/nativeRepl.ts index 8b233f765468..f11f5c0ebd74 100644 --- a/src/client/repl/nativeRepl.ts +++ b/src/client/repl/nativeRepl.ts @@ -19,6 +19,7 @@ import { executeNotebookCell, openInteractiveREPL, selectNotebookKernel } from ' import { createReplController } from './replController'; import { EventName } from '../telemetry/constants'; import { sendTelemetryEvent } from '../telemetry'; +import { VariablesProvider } from './variables/variablesProvider'; let nativeRepl: NativeRepl | undefined; // In multi REPL scenario, hashmap of URI to Repl. export class NativeRepl implements Disposable { @@ -48,7 +49,7 @@ export class NativeRepl implements Disposable { nativeRepl.interpreter = interpreter; await nativeRepl.setReplDirectory(); nativeRepl.pythonServer = createPythonServer([interpreter.path as string], nativeRepl.cwd); - nativeRepl.replController = nativeRepl.setReplController(); + nativeRepl.setReplController(); return nativeRepl; } @@ -111,7 +112,8 @@ export class NativeRepl implements Disposable { */ public setReplController(): NotebookController { if (!this.replController) { - return createReplController(this.interpreter!.path, this.disposables, this.cwd); + this.replController = createReplController(this.interpreter!.path, this.disposables, this.cwd); + this.replController.variableProvider = new VariablesProvider(this.pythonServer); } return this.replController; } diff --git a/src/client/repl/pythonServer.ts b/src/client/repl/pythonServer.ts index a342e989af7c..b128360bc7df 100644 --- a/src/client/repl/pythonServer.ts +++ b/src/client/repl/pythonServer.ts @@ -21,7 +21,7 @@ export interface PythonServer extends Disposable { checkValidCommand(code: string): Promise; } -class PythonServerImpl implements Disposable { +class PythonServerImpl implements PythonServer, Disposable { private readonly disposables: Disposable[] = []; constructor(private connection: rpc.MessageConnection, private pythonServer: ch.ChildProcess) { diff --git a/src/client/repl/variables/types.ts b/src/client/repl/variables/types.ts new file mode 100644 index 000000000000..1e3c80d32077 --- /dev/null +++ b/src/client/repl/variables/types.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { CancellationToken, Variable } from 'vscode'; + +export interface IVariableDescription extends Variable { + /** The name of the variable at the root scope */ + root: string; + /** How to look up the specific property of the root variable */ + propertyChain: (string | number)[]; + /** The number of children for collection types */ + count?: number; + /** Names of children */ + hasNamedChildren?: boolean; + /** A method to get the children of this variable */ + getChildren?: (start: number, token: CancellationToken) => Promise; +} diff --git a/src/client/repl/variables/variableResultCache.ts b/src/client/repl/variables/variableResultCache.ts new file mode 100644 index 000000000000..fa26ff80ca21 --- /dev/null +++ b/src/client/repl/variables/variableResultCache.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { VariablesResult } from 'vscode'; + +export class VariableResultCacheBase { + private cache = new Map(); + + private executionCount = 0; + + getResults(executionCount: number, cacheKey: string): T | undefined { + if (this.executionCount !== executionCount) { + this.cache.clear(); + this.executionCount = executionCount; + } + + return this.cache.get(cacheKey); + } + + setResults(executionCount: number, cacheKey: string, results: T): void { + if (this.executionCount < executionCount) { + this.cache.clear(); + this.executionCount = executionCount; + } else if (this.executionCount > executionCount) { + // old results, don't cache + return; + } + + this.cache.set(cacheKey, results); + } +} + +export const VariableResultCache = VariableResultCacheBase; diff --git a/src/client/repl/variables/variablesProvider.ts b/src/client/repl/variables/variablesProvider.ts new file mode 100644 index 000000000000..12c8c76314b3 --- /dev/null +++ b/src/client/repl/variables/variablesProvider.ts @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + CancellationToken, + NotebookDocument, + Variable, + NotebookVariablesRequestKind, + VariablesResult, + EventEmitter, + NotebookVariableProvider, +} from 'vscode'; +import * as path from 'path'; +import * as fsapi from '../../common/platform/fs-paths'; +import { VariableResultCache } from './variableResultCache'; +import { PythonServer } from '../pythonServer'; +import { IVariableDescription } from './types'; +import { EXTENSION_ROOT_DIR } from '../../constants'; + +const VARIABLE_SCRIPT_LOCATION = path.join(EXTENSION_ROOT_DIR, 'python_files', 'get_variable_info.py'); + +export class VariablesProvider implements NotebookVariableProvider { + public static scriptContents: string | undefined; + + private variableResultCache = new VariableResultCache(); + + private _onDidChangeVariables = new EventEmitter(); + + onDidChangeVariables = this._onDidChangeVariables.event; + + private executionCount = 0; + + constructor(private readonly pythonServer: PythonServer) {} + + // TODO: signal that variables have chagned when the server executes user code + + async *provideVariables( + notebook: NotebookDocument, + parent: Variable | undefined, + kind: NotebookVariablesRequestKind, + start: number, + token: CancellationToken, + ): AsyncIterable { + // TODO: check if server is running + if (token.isCancellationRequested) { + return; + } + + // eslint-disable-next-line no-plusplus + const executionCount = this.executionCount++; + + const cacheKey = getVariableResultCacheKey(notebook.uri.toString(), parent, start); + let results = this.variableResultCache.getResults(executionCount, cacheKey); + + if (parent) { + const parentDescription = parent as IVariableDescription; + if (!results && parentDescription.getChildren) { + const variables = await parentDescription.getChildren(start, token); + results = variables.map((variable) => this.createVariableResult(variable)); + this.variableResultCache.setResults(executionCount, cacheKey, results); + } else if (!results) { + // no cached results and no way to get children, so return empty + return; + } + + for (const result of results) { + yield result; + } + + // check if we have more indexed children to return + if ( + kind === 2 && + parentDescription.count && + results.length > 0 && + parentDescription.count > start + results.length + ) { + for await (const result of this.provideVariables( + notebook, + parent, + kind, + start + results.length, + token, + )) { + yield result; + } + } + } else { + if (!results) { + const variables = await this.getAllVariableDiscriptions(undefined, start, token); + results = variables.map((variable) => this.createVariableResult(variable)); + this.variableResultCache.setResults(executionCount, cacheKey, results); + } + + for (const result of results) { + yield result; + } + } + } + + private createVariableResult(result: IVariableDescription): VariablesResult { + const indexedChildrenCount = result.count ?? 0; + const hasNamedChildren = !!result.hasNamedChildren; + const variable = { + getChildren: (start: number, token: CancellationToken) => this.getChildren(variable, start, token), + expression: createExpression(result.root, result.propertyChain), + ...result, + } as Variable; + return { variable, hasNamedChildren, indexedChildrenCount }; + } + + async getChildren(variable: Variable, start: number, token: CancellationToken): Promise { + const parent = variable as IVariableDescription; + return this.getAllVariableDiscriptions(parent, start, token); + } + + async getAllVariableDiscriptions( + parent: IVariableDescription | undefined, + start: number, + token: CancellationToken, + ): Promise { + let scriptCode = await getContentsOfVariablesScript(); + if (parent) { + scriptCode = `${scriptCode}\n\nreturn _VSCODE_getAllChildrenDescriptions(\'${ + parent.root + }\', ${JSON.stringify(parent.propertyChain)}, ${start})`; + } else { + scriptCode = `${scriptCode}\n\nvariables= locals()\nreturn _VSCODE_getVariableDescriptions(variables)`; + } + + if (token.isCancellationRequested) { + return []; + } + + const result = await this.pythonServer.execute(scriptCode); + + if (result?.output && token.isCancellationRequested) { + return JSON.parse(result.output) as IVariableDescription[]; + } + + return []; + } +} + +async function getContentsOfVariablesScript(): Promise { + if (VariablesProvider.scriptContents) { + return VariablesProvider.scriptContents; + } + const contents = await fsapi.readFile(VARIABLE_SCRIPT_LOCATION, 'utf-8'); + VariablesProvider.scriptContents = contents; + return Promise.resolve('locals()'); +} + +function createExpression(root: string, propertyChain: (string | number)[]): string { + let expression = root; + for (const property of propertyChain) { + if (typeof property === 'string') { + expression += `.${property}`; + } else { + expression += `[${property}]`; + } + } + return expression; +} + +function getVariableResultCacheKey(notebookUri: string, parent: Variable | undefined, start: number) { + let parentKey = ''; + const parentDescription = parent as IVariableDescription; + if (parentDescription) { + parentKey = `${parentDescription.name}.${parentDescription.propertyChain.join('.')}[[${start}`; + } + return `${notebookUri}:${parentKey}`; +} diff --git a/types/vscode.proposed.notebookVariableProvider.d.ts b/types/vscode.proposed.notebookVariableProvider.d.ts new file mode 100644 index 000000000000..4fac96c45f0a --- /dev/null +++ b/types/vscode.proposed.notebookVariableProvider.d.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +declare module 'vscode' { + + export interface NotebookController { + /** Set this to attach a variable provider to this controller. */ + variableProvider?: NotebookVariableProvider; + } + + export enum NotebookVariablesRequestKind { + Named = 1, + Indexed = 2 + } + + interface VariablesResult { + variable: Variable; + hasNamedChildren: boolean; + indexedChildrenCount: number; + } + + interface NotebookVariableProvider { + onDidChangeVariables: Event; + + /** When parent is undefined, this is requesting global Variables. When a variable is passed, it's requesting child props of that Variable. */ + provideVariables(notebook: NotebookDocument, parent: Variable | undefined, kind: NotebookVariablesRequestKind, start: number, token: CancellationToken): AsyncIterable; + } + + interface Variable { + /** The variable's name. */ + name: string; + + /** The variable's value. + This can be a multi-line text, e.g. for a function the body of a function. + For structured variables (which do not have a simple value), it is recommended to provide a one-line representation of the structured object. + This helps to identify the structured object in the collapsed state when its children are not yet visible. + An empty string can be used if no value should be shown in the UI. + */ + value: string; + + /** The code that represents how the variable would be accessed in the runtime environment */ + expression?: string; + + /** The type of the variable's value */ + type?: string; + + /** The interfaces or contracts that the type satisfies */ + interfaces?: string[]; + + /** The language of the variable's value */ + language?: string; + } + +} From d5e1b110787d15c03623805787a2cca26dd01092 Mon Sep 17 00:00:00 2001 From: amunger Date: Wed, 11 Sep 2024 10:45:17 -0700 Subject: [PATCH 02/23] working variable provider --- python_files/get_variable_info.py | 51 ++++++------------- .../repl/variables/variablesProvider.ts | 10 ++-- 2 files changed, 21 insertions(+), 40 deletions(-) diff --git a/python_files/get_variable_info.py b/python_files/get_variable_info.py index 8ee8cf30371d..58f41bf453d0 100644 --- a/python_files/get_variable_info.py +++ b/python_files/get_variable_info.py @@ -177,9 +177,7 @@ def _is_long_iter(self, obj, level=0): size = None if size is not None and size > self.maxcollection[level]: return True - return any( - self._is_long_iter(item, level + 1) for item in obj - ) + return any(self._is_long_iter(item, level + 1) for item in obj) return any( i > self.maxcollection[level] or self._is_long_iter(item, level + 1) for i, item in enumerate(obj) @@ -225,9 +223,7 @@ def _repr_long_iter(self, obj): obj_repr = "" yield obj_repr - def _repr_dict( - self, obj, level, prefix, suffix, item_prefix, item_sep, item_suffix - ): + def _repr_dict(self, obj, level, prefix, suffix, item_prefix, item_sep, item_suffix): if not obj: yield prefix + suffix return @@ -294,9 +290,7 @@ def _repr_str(self, obj, level): # Slightly imprecise calculations - we may end up with a string that is # up to 6 characters longer than limit. If you need precise formatting, # you are using the wrong class. - left_count, right_count = max(1, int(2 * limit / 3)), max( - 1, int(limit / 3) - ) + left_count, right_count = max(1, int(2 * limit / 3)), max(1, int(limit / 3)) # Important: only do repr after slicing to avoid duplicating a byte array that could be # huge. @@ -316,9 +310,7 @@ def _repr_str(self, obj, level): part2 = obj[-right_count:] part2 = repr(part2) - part2 = part2[ - part2.index("'") + 1 : - ] # Remove the first ' (and possibly u or b). + part2 = part2[part2.index("'") + 1 :] # Remove the first ' (and possibly u or b). yield part1 yield "..." @@ -326,9 +318,7 @@ def _repr_str(self, obj, level): except: # noqa: E722 # This shouldn't really happen, but let's play it safe. # exception('Error getting string representation to show.') - yield from self._repr_obj( - obj, level, self.maxother_inner, self.maxother_outer - ) + yield from self._repr_obj(obj, level, self.maxother_inner, self.maxother_outer) def _repr_other(self, obj, level): return self._repr_obj(obj, level, self.maxother_inner, self.maxother_outer) @@ -359,9 +349,7 @@ def _repr_obj(self, obj, level, limit_inner, limit_outer): obj_repr = object.__repr__(obj) except Exception: try: - obj_repr = ( - "" - ) + obj_repr = "" except Exception: obj_repr = "" @@ -374,9 +362,7 @@ def _repr_obj(self, obj, level, limit_inner, limit_outer): # Slightly imprecise calculations - we may end up with a string that is # up to 3 characters longer than limit. If you need precise formatting, # you are using the wrong class. - left_count, right_count = max(1, int(2 * limit / 3)), max( - 1, int(limit / 3) - ) + left_count, right_count = max(1, int(2 * limit / 3)), max(1, int(limit / 3)) yield obj_repr[:left_count] yield "..." @@ -394,9 +380,7 @@ def _bytes_as_unicode_if_possible(self, obj_repr): if encoding: try_encodings.append(encoding.lower()) - preferred_encoding = ( - self.locale_preferred_encoding or locale.getpreferredencoding() - ) + preferred_encoding = self.locale_preferred_encoding or locale.getpreferredencoding() if preferred_encoding: preferred_encoding = preferred_encoding.lower() if preferred_encoding not in try_encodings: @@ -414,12 +398,15 @@ def _bytes_as_unicode_if_possible(self, obj_repr): return obj_repr # Return the original version (in bytes) +class DisplayOptions(NamedTuple): + width: int + max_columns: int + + safe_repr = SafeRepr() collection_types = ["list", "tuple", "set"] array_page_size = 50 -DisplayOptions = NamedTuple("DisplayOptions", ["width", "max_columns"]) - def set_pandas_display_options(display_options=None): if find_spec("pandas") is not None: @@ -494,9 +481,7 @@ def get_variable_description(variable): if hasattr(variable, "__len__") and result["type"] in collection_types: result["count"] = len(variable) - result["hasNamedChildren"] = hasattr(variable, "__dict__") or isinstance( - variable, dict - ) + result["hasNamedChildren"] = hasattr(variable, "__dict__") or isinstance(variable, dict) result["value"] = get_value(variable) return result @@ -536,8 +521,7 @@ def _VSCODE_getVariableDescriptions(var_names): # noqa: N802 "language": "python", } for varName in var_names - if varName in globals() - and type(globals()[varName]).__name__ not in types_to_exclude + if varName in globals() and type(globals()[varName]).__name__ not in types_to_exclude ] return json.dumps(variables) @@ -579,10 +563,7 @@ def _VSCODE_getAllChildrenDescriptions(root_var_name, property_chain, start_inde children = [] for prop in children_names: child_property = get_child_property(parent, [prop]) - if ( - child_property is not None - and type(child_property).__name__ not in types_to_exclude - ): + if child_property is not None and type(child_property).__name__ not in types_to_exclude: child = { **get_variable_description(child_property), "name": prop, diff --git a/src/client/repl/variables/variablesProvider.ts b/src/client/repl/variables/variablesProvider.ts index 12c8c76314b3..9c411d3c0989 100644 --- a/src/client/repl/variables/variablesProvider.ts +++ b/src/client/repl/variables/variablesProvider.ts @@ -120,11 +120,11 @@ export class VariablesProvider implements NotebookVariableProvider { ): Promise { let scriptCode = await getContentsOfVariablesScript(); if (parent) { - scriptCode = `${scriptCode}\n\nreturn _VSCODE_getAllChildrenDescriptions(\'${ + scriptCode = `${scriptCode}\r\n\r\nprint(_VSCODE_getAllChildrenDescriptions(\'${ parent.root - }\', ${JSON.stringify(parent.propertyChain)}, ${start})`; + }\', ${JSON.stringify(parent.propertyChain)}, ${start}))`; } else { - scriptCode = `${scriptCode}\n\nvariables= locals()\nreturn _VSCODE_getVariableDescriptions(variables)`; + scriptCode = `${scriptCode}\r\n\r\nvariables= locals()\r\nprint(_VSCODE_getVariableDescriptions(variables))`; } if (token.isCancellationRequested) { @@ -133,7 +133,7 @@ export class VariablesProvider implements NotebookVariableProvider { const result = await this.pythonServer.execute(scriptCode); - if (result?.output && token.isCancellationRequested) { + if (result?.output && !token.isCancellationRequested) { return JSON.parse(result.output) as IVariableDescription[]; } @@ -147,7 +147,7 @@ async function getContentsOfVariablesScript(): Promise { } const contents = await fsapi.readFile(VARIABLE_SCRIPT_LOCATION, 'utf-8'); VariablesProvider.scriptContents = contents; - return Promise.resolve('locals()'); + return VariablesProvider.scriptContents; } function createExpression(root: string, propertyChain: (string | number)[]): string { From ee0d2550454c01e69ff50b2e6b9aa447c39139c9 Mon Sep 17 00:00:00 2001 From: amunger Date: Wed, 11 Sep 2024 13:30:56 -0700 Subject: [PATCH 03/23] clean up scope, scan globals() --- python_files/get_variable_info.py | 14 ++++++------- .../repl/variables/variablesProvider.ts | 21 +++++++++++++------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/python_files/get_variable_info.py b/python_files/get_variable_info.py index 58f41bf453d0..96706de398c2 100644 --- a/python_files/get_variable_info.py +++ b/python_files/get_variable_info.py @@ -10,7 +10,7 @@ from typing import NamedTuple -# The pydevd SafeRepr class used in ptvsd/debugpy +# this class is from in ptvsd/debugpy tools class SafeRepr(object): # noqa: UP004 # Can be used to override the encoding from locale.getpreferredencoding() locale_preferred_encoding = None @@ -467,9 +467,6 @@ def get_full_type(var_type): return None -types_to_exclude = ["module", "function", "method", "class", "type"] - - def get_variable_description(variable): result = {} @@ -510,8 +507,11 @@ def get_child_property(root, property_chain): return variable +types_to_exclude = ["module", "function", "method", "class", "type"] + + ### Get info on variables at the root level -def _VSCODE_getVariableDescriptions(var_names): # noqa: N802 +def _VSCODE_getVariableDescriptions(): # noqa: N802 variables = [ { "name": varName, @@ -520,8 +520,8 @@ def _VSCODE_getVariableDescriptions(var_names): # noqa: N802 "propertyChain": [], "language": "python", } - for varName in var_names - if varName in globals() and type(globals()[varName]).__name__ not in types_to_exclude + for varName in globals() + if type(globals()[varName]).__name__ not in types_to_exclude and not varName.startswith("__") ] return json.dumps(variables) diff --git a/src/client/repl/variables/variablesProvider.ts b/src/client/repl/variables/variablesProvider.ts index 9c411d3c0989..cc77af0ddf7a 100644 --- a/src/client/repl/variables/variablesProvider.ts +++ b/src/client/repl/variables/variablesProvider.ts @@ -118,20 +118,22 @@ export class VariablesProvider implements NotebookVariableProvider { start: number, token: CancellationToken, ): Promise { - let scriptCode = await getContentsOfVariablesScript(); + const scriptLines = (await getContentsOfVariablesScript()).split(/(?:\r\n|\n)/); if (parent) { - scriptCode = `${scriptCode}\r\n\r\nprint(_VSCODE_getAllChildrenDescriptions(\'${ - parent.root - }\', ${JSON.stringify(parent.propertyChain)}, ${start}))`; + const printCall = `return _VSCODE_getAllChildrenDescriptions(\'${parent.root}\', ${JSON.stringify( + parent.propertyChain, + )}, ${start})`; + scriptLines.push(printCall); } else { - scriptCode = `${scriptCode}\r\n\r\nvariables= locals()\r\nprint(_VSCODE_getVariableDescriptions(variables))`; + scriptLines.push('return _VSCODE_getVariableDescriptions()'); } if (token.isCancellationRequested) { return []; } - const result = await this.pythonServer.execute(scriptCode); + const script = wrapScriptInFunction(scriptLines); + const result = await this.pythonServer.execute(script); if (result?.output && !token.isCancellationRequested) { return JSON.parse(result.output) as IVariableDescription[]; @@ -141,6 +143,13 @@ export class VariablesProvider implements NotebookVariableProvider { } } +function wrapScriptInFunction(scriptLines: string[]): string { + const indented = scriptLines.map((line) => ` ${line}`).join('\n'); + // put everything into a function scope and then delete that scope + // TODO: run in a background thread + return `def __VSCODE_run_script():\n${indented}\nprint(__VSCODE_run_script())\ndel __VSCODE_run_script`; +} + async function getContentsOfVariablesScript(): Promise { if (VariablesProvider.scriptContents) { return VariablesProvider.scriptContents; From 505089a73787a41042af8f6f89fba1937b0e9656 Mon Sep 17 00:00:00 2001 From: amunger Date: Wed, 11 Sep 2024 15:15:55 -0700 Subject: [PATCH 04/23] unit tests --- src/client/repl/nativeRepl.ts | 7 +- src/client/repl/pythonServer.ts | 20 +++- .../repl/variables/variableResultCache.ts | 3 - .../repl/variables/variablesProvider.ts | 92 ++++++------------- 4 files changed, 54 insertions(+), 68 deletions(-) diff --git a/src/client/repl/nativeRepl.ts b/src/client/repl/nativeRepl.ts index f11f5c0ebd74..a1c5c5508851 100644 --- a/src/client/repl/nativeRepl.ts +++ b/src/client/repl/nativeRepl.ts @@ -20,6 +20,7 @@ import { createReplController } from './replController'; import { EventName } from '../telemetry/constants'; import { sendTelemetryEvent } from '../telemetry'; import { VariablesProvider } from './variables/variablesProvider'; +import { VariableRequester } from './variables/variableRequester'; let nativeRepl: NativeRepl | undefined; // In multi REPL scenario, hashmap of URI to Repl. export class NativeRepl implements Disposable { @@ -113,7 +114,11 @@ export class NativeRepl implements Disposable { public setReplController(): NotebookController { if (!this.replController) { this.replController = createReplController(this.interpreter!.path, this.disposables, this.cwd); - this.replController.variableProvider = new VariablesProvider(this.pythonServer); + this.replController.variableProvider = new VariablesProvider( + this.pythonServer, + new VariableRequester(this.pythonServer), + () => this.notebookDocument, + ); } return this.replController; } diff --git a/src/client/repl/pythonServer.ts b/src/client/repl/pythonServer.ts index b128360bc7df..0f500f0431bc 100644 --- a/src/client/repl/pythonServer.ts +++ b/src/client/repl/pythonServer.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import * as ch from 'child_process'; import * as rpc from 'vscode-jsonrpc/node'; -import { Disposable, window } from 'vscode'; +import { Disposable, Event, EventEmitter, window } from 'vscode'; import { EXTENSION_ROOT_DIR } from '../constants'; import { traceError, traceLog } from '../logging'; import { captureTelemetry } from '../telemetry'; @@ -15,7 +15,9 @@ export interface ExecutionResult { } export interface PythonServer extends Disposable { + onCodeExecuted: Event; execute(code: string): Promise; + executeSilently(code: string): Promise; interrupt(): void; input(): void; checkValidCommand(code: string): Promise; @@ -24,6 +26,10 @@ export interface PythonServer extends Disposable { class PythonServerImpl implements PythonServer, Disposable { private readonly disposables: Disposable[] = []; + private readonly _onCodeExecuted = new EventEmitter(); + + onCodeExecuted = this._onCodeExecuted.event; + constructor(private connection: rpc.MessageConnection, private pythonServer: ch.ChildProcess) { this.initialize(); this.input(); @@ -57,6 +63,18 @@ class PythonServerImpl implements PythonServer, Disposable { @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'selection' }, false) public async execute(code: string): Promise { + const result = await this.executeCode(code); + if (result?.status) { + this._onCodeExecuted.fire(); + } + return result; + } + + public executeSilently(code: string): Promise { + return this.executeCode(code); + } + + private async executeCode(code: string): Promise { try { const result = await this.connection.sendRequest('execute', code); return result as ExecutionResult; diff --git a/src/client/repl/variables/variableResultCache.ts b/src/client/repl/variables/variableResultCache.ts index fa26ff80ca21..a34707fd8bb2 100644 --- a/src/client/repl/variables/variableResultCache.ts +++ b/src/client/repl/variables/variableResultCache.ts @@ -1,6 +1,3 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - import { VariablesResult } from 'vscode'; export class VariableResultCacheBase { diff --git a/src/client/repl/variables/variablesProvider.ts b/src/client/repl/variables/variablesProvider.ts index cc77af0ddf7a..7bc80323d41c 100644 --- a/src/client/repl/variables/variablesProvider.ts +++ b/src/client/repl/variables/variablesProvider.ts @@ -10,19 +10,13 @@ import { EventEmitter, NotebookVariableProvider, } from 'vscode'; -import * as path from 'path'; -import * as fsapi from '../../common/platform/fs-paths'; import { VariableResultCache } from './variableResultCache'; import { PythonServer } from '../pythonServer'; import { IVariableDescription } from './types'; -import { EXTENSION_ROOT_DIR } from '../../constants'; - -const VARIABLE_SCRIPT_LOCATION = path.join(EXTENSION_ROOT_DIR, 'python_files', 'get_variable_info.py'); +import { VariableRequester } from './variableRequester'; export class VariablesProvider implements NotebookVariableProvider { - public static scriptContents: string | undefined; - - private variableResultCache = new VariableResultCache(); + private readonly variableResultCache = new VariableResultCache(); private _onDidChangeVariables = new EventEmitter(); @@ -30,9 +24,21 @@ export class VariablesProvider implements NotebookVariableProvider { private executionCount = 0; - constructor(private readonly pythonServer: PythonServer) {} + constructor( + private readonly pythonServer: PythonServer, + private readonly variableRequester: VariableRequester, + private readonly getNotebookDocument: () => NotebookDocument | undefined, + ) { + this.pythonServer.onCodeExecuted(() => this.onDidExecuteCode()); + } - // TODO: signal that variables have chagned when the server executes user code + onDidExecuteCode(): void { + const notebook = this.getNotebookDocument(); + if (notebook) { + this.executionCount += 1; + this._onDidChangeVariables.fire(notebook); + } + } async *provideVariables( notebook: NotebookDocument, @@ -42,13 +48,12 @@ export class VariablesProvider implements NotebookVariableProvider { token: CancellationToken, ): AsyncIterable { // TODO: check if server is running - if (token.isCancellationRequested) { + const notebookDocument = this.getNotebookDocument(); + if (token.isCancellationRequested || !notebookDocument || notebookDocument !== notebook) { return; } - // eslint-disable-next-line no-plusplus - const executionCount = this.executionCount++; - + const { executionCount } = this; const cacheKey = getVariableResultCacheKey(notebook.uri.toString(), parent, start); let results = this.variableResultCache.getResults(executionCount, cacheKey); @@ -56,6 +61,9 @@ export class VariablesProvider implements NotebookVariableProvider { const parentDescription = parent as IVariableDescription; if (!results && parentDescription.getChildren) { const variables = await parentDescription.getChildren(start, token); + if (token.isCancellationRequested) { + return; + } results = variables.map((variable) => this.createVariableResult(variable)); this.variableResultCache.setResults(executionCount, cacheKey, results); } else if (!results) { @@ -86,7 +94,10 @@ export class VariablesProvider implements NotebookVariableProvider { } } else { if (!results) { - const variables = await this.getAllVariableDiscriptions(undefined, start, token); + const variables = await this.variableRequester.getAllVariableDescriptions(undefined, start, token); + if (token.isCancellationRequested) { + return; + } results = variables.map((variable) => this.createVariableResult(variable)); this.variableResultCache.setResults(executionCount, cacheKey, results); } @@ -110,53 +121,8 @@ export class VariablesProvider implements NotebookVariableProvider { async getChildren(variable: Variable, start: number, token: CancellationToken): Promise { const parent = variable as IVariableDescription; - return this.getAllVariableDiscriptions(parent, start, token); - } - - async getAllVariableDiscriptions( - parent: IVariableDescription | undefined, - start: number, - token: CancellationToken, - ): Promise { - const scriptLines = (await getContentsOfVariablesScript()).split(/(?:\r\n|\n)/); - if (parent) { - const printCall = `return _VSCODE_getAllChildrenDescriptions(\'${parent.root}\', ${JSON.stringify( - parent.propertyChain, - )}, ${start})`; - scriptLines.push(printCall); - } else { - scriptLines.push('return _VSCODE_getVariableDescriptions()'); - } - - if (token.isCancellationRequested) { - return []; - } - - const script = wrapScriptInFunction(scriptLines); - const result = await this.pythonServer.execute(script); - - if (result?.output && !token.isCancellationRequested) { - return JSON.parse(result.output) as IVariableDescription[]; - } - - return []; - } -} - -function wrapScriptInFunction(scriptLines: string[]): string { - const indented = scriptLines.map((line) => ` ${line}`).join('\n'); - // put everything into a function scope and then delete that scope - // TODO: run in a background thread - return `def __VSCODE_run_script():\n${indented}\nprint(__VSCODE_run_script())\ndel __VSCODE_run_script`; -} - -async function getContentsOfVariablesScript(): Promise { - if (VariablesProvider.scriptContents) { - return VariablesProvider.scriptContents; + return this.variableRequester.getAllVariableDescriptions(parent, start, token); } - const contents = await fsapi.readFile(VARIABLE_SCRIPT_LOCATION, 'utf-8'); - VariablesProvider.scriptContents = contents; - return VariablesProvider.scriptContents; } function createExpression(root: string, propertyChain: (string | number)[]): string { @@ -171,11 +137,11 @@ function createExpression(root: string, propertyChain: (string | number)[]): str return expression; } -function getVariableResultCacheKey(notebookUri: string, parent: Variable | undefined, start: number) { +function getVariableResultCacheKey(uri: string, parent: Variable | undefined, start: number) { let parentKey = ''; const parentDescription = parent as IVariableDescription; if (parentDescription) { parentKey = `${parentDescription.name}.${parentDescription.propertyChain.join('.')}[[${start}`; } - return `${notebookUri}:${parentKey}`; + return `${uri}:${parentKey}`; } From c89cbc75133b11be5c7262f85d35cb5efdb37438 Mon Sep 17 00:00:00 2001 From: amunger Date: Wed, 11 Sep 2024 15:56:17 -0700 Subject: [PATCH 05/23] switch to typemoq... --- src/client/repl/variables/variablesProvider.ts | 7 +++---- src/test/.vscode/settings.json | 3 +-- src/testMultiRootWkspc/multi.code-workspace | 3 ++- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/client/repl/variables/variablesProvider.ts b/src/client/repl/variables/variablesProvider.ts index 7bc80323d41c..69e595a5a74c 100644 --- a/src/client/repl/variables/variablesProvider.ts +++ b/src/client/repl/variables/variablesProvider.ts @@ -8,10 +8,10 @@ import { NotebookVariablesRequestKind, VariablesResult, EventEmitter, + Event, NotebookVariableProvider, } from 'vscode'; import { VariableResultCache } from './variableResultCache'; -import { PythonServer } from '../pythonServer'; import { IVariableDescription } from './types'; import { VariableRequester } from './variableRequester'; @@ -25,11 +25,11 @@ export class VariablesProvider implements NotebookVariableProvider { private executionCount = 0; constructor( - private readonly pythonServer: PythonServer, private readonly variableRequester: VariableRequester, private readonly getNotebookDocument: () => NotebookDocument | undefined, + codeExecutedEvent: Event ) { - this.pythonServer.onCodeExecuted(() => this.onDidExecuteCode()); + codeExecutedEvent(() => this.onDidExecuteCode()); } onDidExecuteCode(): void { @@ -47,7 +47,6 @@ export class VariablesProvider implements NotebookVariableProvider { start: number, token: CancellationToken, ): AsyncIterable { - // TODO: check if server is running const notebookDocument = this.getNotebookDocument(); if (token.isCancellationRequested || !notebookDocument || notebookDocument !== notebook) { return; diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json index cd2b4152591d..faeb48ffa29c 100644 --- a/src/test/.vscode/settings.json +++ b/src/test/.vscode/settings.json @@ -13,6 +13,5 @@ "python.linting.banditEnabled": false, // Don't set this to `Pylance`, for CI we want to use the LS that ships with the extension. "python.languageServer": "Jedi", - "python.pythonPath": "C:\\GIT\\s p\\vscode-python\\.venv\\Scripts\\python.exe", - "python.defaultInterpreterPath": "python" + "python.pythonPath": "C:\\GIT\\s p\\vscode-python\\.venv\\Scripts\\python.exe" } diff --git a/src/testMultiRootWkspc/multi.code-workspace b/src/testMultiRootWkspc/multi.code-workspace index 51d218783041..5ed4fa94902f 100644 --- a/src/testMultiRootWkspc/multi.code-workspace +++ b/src/testMultiRootWkspc/multi.code-workspace @@ -39,6 +39,7 @@ "python.linting.prospectorEnabled": false, "python.linting.lintOnSave": false, "python.linting.enabled": true, - "python.pythonPath": "python" + "python.pythonPath": "python", + "python.defaultInterpreterPath": "python" } } From 2cee0c37baa417a3bf5272b49103b47cfdf1cda7 Mon Sep 17 00:00:00 2001 From: amunger Date: Thu, 12 Sep 2024 09:25:45 -0700 Subject: [PATCH 06/23] actually add the unit tests --- src/client/repl/nativeRepl.ts | 2 +- .../repl/variables/variableRequester.ts | 59 +++++ src/test/repl/variableProvider.test.ts | 246 ++++++++++++++++++ 3 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 src/client/repl/variables/variableRequester.ts create mode 100644 src/test/repl/variableProvider.test.ts diff --git a/src/client/repl/nativeRepl.ts b/src/client/repl/nativeRepl.ts index a1c5c5508851..8e0337f8d276 100644 --- a/src/client/repl/nativeRepl.ts +++ b/src/client/repl/nativeRepl.ts @@ -115,9 +115,9 @@ export class NativeRepl implements Disposable { if (!this.replController) { this.replController = createReplController(this.interpreter!.path, this.disposables, this.cwd); this.replController.variableProvider = new VariablesProvider( - this.pythonServer, new VariableRequester(this.pythonServer), () => this.notebookDocument, + this.pythonServer.onCodeExecuted, ); } return this.replController; diff --git a/src/client/repl/variables/variableRequester.ts b/src/client/repl/variables/variableRequester.ts new file mode 100644 index 000000000000..93ddf053274c --- /dev/null +++ b/src/client/repl/variables/variableRequester.ts @@ -0,0 +1,59 @@ +import { CancellationToken } from 'vscode'; +import path from 'path'; +import * as fsapi from '../../common/platform/fs-paths'; +import { IVariableDescription } from './types'; +import { PythonServer } from '../pythonServer'; +import { EXTENSION_ROOT_DIR } from '../../constants'; + +const VARIABLE_SCRIPT_LOCATION = path.join(EXTENSION_ROOT_DIR, 'python_files', 'get_variable_info.py'); + +export class VariableRequester { + public static scriptContents: string | undefined; + + constructor(private pythonServer: PythonServer) {} + + async getAllVariableDescriptions( + parent: IVariableDescription | undefined, + start: number, + token: CancellationToken, + ): Promise { + const scriptLines = (await getContentsOfVariablesScript()).split(/(?:\r\n|\n)/); + if (parent) { + const printCall = `return _VSCODE_getAllChildrenDescriptions(\'${parent.root}\', ${JSON.stringify( + parent.propertyChain, + )}, ${start})`; + scriptLines.push(printCall); + } else { + scriptLines.push('return _VSCODE_getVariableDescriptions()'); + } + + if (token.isCancellationRequested) { + return []; + } + + const script = wrapScriptInFunction(scriptLines); + const result = await this.pythonServer.executeSilently(script); + + if (result?.output && !token.isCancellationRequested) { + return JSON.parse(result.output) as IVariableDescription[]; + } + + return []; + } +} + +function wrapScriptInFunction(scriptLines: string[]): string { + const indented = scriptLines.map((line) => ` ${line}`).join('\n'); + // put everything into a function scope and then delete that scope + // TODO: run in a background thread + return `def __VSCODE_run_script():\n${indented}\nprint(__VSCODE_run_script())\ndel __VSCODE_run_script`; +} + +async function getContentsOfVariablesScript(): Promise { + if (VariableRequester.scriptContents) { + return VariableRequester.scriptContents; + } + const contents = await fsapi.readFile(VARIABLE_SCRIPT_LOCATION, 'utf-8'); + VariableRequester.scriptContents = contents; + return VariableRequester.scriptContents; +} diff --git a/src/test/repl/variableProvider.test.ts b/src/test/repl/variableProvider.test.ts new file mode 100644 index 000000000000..fa139759cdcd --- /dev/null +++ b/src/test/repl/variableProvider.test.ts @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import { NotebookDocument, CancellationTokenSource, VariablesResult, Variable, EventEmitter } from 'vscode'; +import * as TypeMoq from 'typemoq'; +import { IVariableDescription } from '../../client/repl/variables/types'; +import { VariablesProvider } from '../../client/repl/variables/variablesProvider'; +import { VariableRequester } from '../../client/repl/variables/variableRequester'; + +suite.only('ReplVariablesProvider', () => { + let provider: VariablesProvider; + let varRequester: TypeMoq.IMock; + let notebook: TypeMoq.IMock; + const executionEventEmitter = new EventEmitter(); + const cancellationToken = new CancellationTokenSource().token; + + const objectVariable: IVariableDescription = { + name: 'myObject', + value: '...', + root: 'myObject', + hasNamedChildren: true, + propertyChain: [], + }; + + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 3, + root: 'myObject', + propertyChain: ['myList'], + }; + + function createListItem(index: number): IVariableDescription { + return { + name: index.toString(), + value: `value${index}`, + count: index, + root: 'myObject', + propertyChain: ['myList', index], + }; + } + + function setVariablesForParent( + parent: IVariableDescription | undefined, + result: IVariableDescription[], + updated?: IVariableDescription[], + startIndex?: number, + ) { + let returnedOnce = false; + varRequester + .setup((v) => v.getAllVariableDescriptions(parent, startIndex ?? TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + if (updated && returnedOnce) { + return Promise.resolve(updated); + } + returnedOnce = true; + return Promise.resolve(result); + }); + } + + async function provideVariables(parent: Variable | undefined, kind = 1) { + const results: VariablesResult[] = []; + for await (const result of provider.provideVariables(notebook.object, parent, kind, 0, cancellationToken)) { + results.push(result); + } + return results; + } + + setup(() => { + varRequester = TypeMoq.Mock.ofType(); + notebook = TypeMoq.Mock.ofType(); + provider = new VariablesProvider(varRequester.object, () => notebook.object, executionEventEmitter.event); + }); + + test('provideVariables without parent should yield variables', async () => { + setVariablesForParent(undefined, [objectVariable]); + + const results = await provideVariables(undefined); + + assert.isNotEmpty(results); + assert.equal(results.length, 1); + assert.equal(results[0].variable.name, 'myObject'); + assert.equal(results[0].variable.expression, 'myObject'); + }); + + test('provideVariables with a parent should call get children correctly', async () => { + const listVariableItems = [0, 1, 2].map(createListItem); + setVariablesForParent(undefined, [objectVariable]); + + // pass each the result as the parent in the next call + const rootVariable = (await provideVariables(undefined))[0]; + setVariablesForParent(rootVariable.variable as IVariableDescription, [listVariable]); + const listResult = (await provideVariables(rootVariable!.variable))[0]; + setVariablesForParent(listResult.variable as IVariableDescription, listVariableItems); + const listItems = await provideVariables(listResult!.variable, 2); + + assert.equal(listResult.variable.name, 'myList'); + assert.equal(listResult.variable.expression, 'myObject.myList'); + assert.isNotEmpty(listItems); + assert.equal(listItems.length, 3); + listItems.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + assert.equal(item.variable.expression, `myObject.myList[${index}]`); + }); + }); + + test('All indexed variables should be returned when requested', async () => { + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 6, + root: 'myList', + propertyChain: [], + }; + + setVariablesForParent(undefined, [listVariable]); + const rootVariable = (await provideVariables(undefined))[0]; + const firstPage = [0, 1, 2].map(createListItem); + const secondPage = [3, 4, 5].map(createListItem); + setVariablesForParent(rootVariable.variable as IVariableDescription, firstPage, undefined, 0); + setVariablesForParent( + rootVariable.variable as IVariableDescription, + secondPage, + undefined, + firstPage.length, + ); + + const listItemResult = await provideVariables(rootVariable!.variable, 2); + + assert.equal(listItemResult.length, 6, 'full list of items should be returned'); + listItemResult.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + }); + }); + + test('Getting less indexed items than the specified count is handled', async () => { + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 6, + root: 'myList', + propertyChain: [], + }; + + const firstPage = [0, 1, 2].map(createListItem); + const secondPage = [3, 4].map(createListItem); + setVariablesForParent(undefined, [listVariable]); + const rootVariable = (await provideVariables(undefined))[0]; + setVariablesForParent(rootVariable.variable as IVariableDescription, firstPage, undefined, 0); + setVariablesForParent( + rootVariable.variable as IVariableDescription, + secondPage, + undefined, + firstPage.length, + ); + setVariablesForParent(rootVariable.variable as IVariableDescription, [], undefined, 5); + + const listItemResult = await provideVariables(rootVariable!.variable, 2); + + assert.equal(listItemResult.length, 5); + listItemResult.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + }); + }); + + test('Getting variables again with new execution count should get updated variables', async () => { + const intVariable: IVariableDescription = { + name: 'myInt', + value: '1', + root: '', + propertyChain: [], + }; + setVariablesForParent(undefined, [intVariable], [{ ...intVariable, value: '2' }]); + + const first = await provideVariables(undefined); + executionEventEmitter.fire(); + const second = await provideVariables(undefined); + + assert.equal(first.length, 1); + assert.equal(second.length, 1); + assert.equal(first[0].variable.value, '1'); + assert.equal(second[0].variable.value, '2'); + }); + + test('Getting variables again with same execution count should not make another call', async () => { + const intVariable: IVariableDescription = { + name: 'myInt', + value: '1', + root: '', + propertyChain: [], + }; + + setVariablesForParent(undefined, [intVariable]); + + const first = await provideVariables(undefined); + const second = await provideVariables(undefined); + + assert.equal(first.length, 1); + assert.equal(second.length, 1); + assert.equal(first[0].variable.value, '1'); + + varRequester.verify(x => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); + + test('Cache pages of indexed children correctly', async () => { + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 6, + root: 'myList', + propertyChain: [], + }; + + const firstPage = [0, 1, 2].map(createListItem); + const secondPage = [3, 4, 5].map(createListItem); + setVariablesForParent(undefined, [listVariable]); + const rootVariable = (await provideVariables(undefined))[0]; + setVariablesForParent(rootVariable.variable as IVariableDescription, firstPage, undefined, 0); + setVariablesForParent( + rootVariable.variable as IVariableDescription, + secondPage, + undefined, + firstPage.length, + ); + + await provideVariables(rootVariable!.variable, 2); + + // once for the parent and once for each of the two pages of list items + varRequester.verify(x => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(3)); + + const listItemResult = await provideVariables(rootVariable!.variable, 2); + + assert.equal(listItemResult.length, 6, 'full list of items should be returned'); + listItemResult.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + }); + + // no extra calls for getting the children again + varRequester.verify(x => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(3)); + }); +}); From acea18ffa7ca05bd862a8197b4e49ca97dd581a4 Mon Sep 17 00:00:00 2001 From: amunger Date: Thu, 12 Sep 2024 09:37:22 -0700 Subject: [PATCH 07/23] bump engine version to get access to variables API --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index c5050689a039..ead6f2b72d04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -116,7 +116,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.89.0-20240415" + "vscode": "^1.94" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index ba8fca9991b1..f66afb61481e 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.89.0-20240415" + "vscode": "^1.94" }, "enableTelemetry": false, "keywords": [ From de50f8c9496232fefa246d936dd4abef3f37f8e0 Mon Sep 17 00:00:00 2001 From: amunger Date: Thu, 12 Sep 2024 09:47:23 -0700 Subject: [PATCH 08/23] lint --- .../repl/variables/variableResultCache.ts | 10 +++--- .../repl/variables/variablesProvider.ts | 2 +- src/test/repl/variableProvider.test.ts | 36 ++++++++----------- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/src/client/repl/variables/variableResultCache.ts b/src/client/repl/variables/variableResultCache.ts index a34707fd8bb2..1e19415becb7 100644 --- a/src/client/repl/variables/variableResultCache.ts +++ b/src/client/repl/variables/variableResultCache.ts @@ -1,11 +1,11 @@ import { VariablesResult } from 'vscode'; -export class VariableResultCacheBase { - private cache = new Map(); +export class VariableResultCache { + private cache = new Map(); private executionCount = 0; - getResults(executionCount: number, cacheKey: string): T | undefined { + getResults(executionCount: number, cacheKey: string): VariablesResult[] | undefined { if (this.executionCount !== executionCount) { this.cache.clear(); this.executionCount = executionCount; @@ -14,7 +14,7 @@ export class VariableResultCacheBase { return this.cache.get(cacheKey); } - setResults(executionCount: number, cacheKey: string, results: T): void { + setResults(executionCount: number, cacheKey: string, results: VariablesResult[]): void { if (this.executionCount < executionCount) { this.cache.clear(); this.executionCount = executionCount; @@ -26,5 +26,3 @@ export class VariableResultCacheBase { this.cache.set(cacheKey, results); } } - -export const VariableResultCache = VariableResultCacheBase; diff --git a/src/client/repl/variables/variablesProvider.ts b/src/client/repl/variables/variablesProvider.ts index 69e595a5a74c..ffb7c221d00c 100644 --- a/src/client/repl/variables/variablesProvider.ts +++ b/src/client/repl/variables/variablesProvider.ts @@ -27,7 +27,7 @@ export class VariablesProvider implements NotebookVariableProvider { constructor( private readonly variableRequester: VariableRequester, private readonly getNotebookDocument: () => NotebookDocument | undefined, - codeExecutedEvent: Event + codeExecutedEvent: Event, ) { codeExecutedEvent(() => this.onDidExecuteCode()); } diff --git a/src/test/repl/variableProvider.test.ts b/src/test/repl/variableProvider.test.ts index fa139759cdcd..8b45fae0c5a0 100644 --- a/src/test/repl/variableProvider.test.ts +++ b/src/test/repl/variableProvider.test.ts @@ -120,12 +120,7 @@ suite.only('ReplVariablesProvider', () => { const firstPage = [0, 1, 2].map(createListItem); const secondPage = [3, 4, 5].map(createListItem); setVariablesForParent(rootVariable.variable as IVariableDescription, firstPage, undefined, 0); - setVariablesForParent( - rootVariable.variable as IVariableDescription, - secondPage, - undefined, - firstPage.length, - ); + setVariablesForParent(rootVariable.variable as IVariableDescription, secondPage, undefined, firstPage.length); const listItemResult = await provideVariables(rootVariable!.variable, 2); @@ -150,12 +145,7 @@ suite.only('ReplVariablesProvider', () => { setVariablesForParent(undefined, [listVariable]); const rootVariable = (await provideVariables(undefined))[0]; setVariablesForParent(rootVariable.variable as IVariableDescription, firstPage, undefined, 0); - setVariablesForParent( - rootVariable.variable as IVariableDescription, - secondPage, - undefined, - firstPage.length, - ); + setVariablesForParent(rootVariable.variable as IVariableDescription, secondPage, undefined, firstPage.length); setVariablesForParent(rootVariable.variable as IVariableDescription, [], undefined, 5); const listItemResult = await provideVariables(rootVariable!.variable, 2); @@ -203,7 +193,10 @@ suite.only('ReplVariablesProvider', () => { assert.equal(second.length, 1); assert.equal(first[0].variable.value, '1'); - varRequester.verify(x => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + varRequester.verify( + (x) => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); test('Cache pages of indexed children correctly', async () => { @@ -220,17 +213,15 @@ suite.only('ReplVariablesProvider', () => { setVariablesForParent(undefined, [listVariable]); const rootVariable = (await provideVariables(undefined))[0]; setVariablesForParent(rootVariable.variable as IVariableDescription, firstPage, undefined, 0); - setVariablesForParent( - rootVariable.variable as IVariableDescription, - secondPage, - undefined, - firstPage.length, - ); + setVariablesForParent(rootVariable.variable as IVariableDescription, secondPage, undefined, firstPage.length); await provideVariables(rootVariable!.variable, 2); // once for the parent and once for each of the two pages of list items - varRequester.verify(x => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(3)); + varRequester.verify( + (x) => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.exactly(3), + ); const listItemResult = await provideVariables(rootVariable!.variable, 2); @@ -241,6 +232,9 @@ suite.only('ReplVariablesProvider', () => { }); // no extra calls for getting the children again - varRequester.verify(x => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(3)); + varRequester.verify( + (x) => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.exactly(3), + ); }); }); From 5da66a7b71cb34174976b2587e0b8c60b80748df Mon Sep 17 00:00:00 2001 From: amunger Date: Thu, 12 Sep 2024 11:01:00 -0700 Subject: [PATCH 09/23] correct version number --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ead6f2b72d04..59795d9523c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -116,7 +116,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.94" + "vscode": "^1.94.0" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index f66afb61481e..5fb6fe4027b3 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.94" + "vscode": "^1.94.0" }, "enableTelemetry": false, "keywords": [ From dbc3b9e20b9007a5044e0f9116f18a74372cd264 Mon Sep 17 00:00:00 2001 From: amunger Date: Thu, 12 Sep 2024 11:28:07 -0700 Subject: [PATCH 10/23] python types --- python_files/get_variable_info.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/python_files/get_variable_info.py b/python_files/get_variable_info.py index 96706de398c2..c1cee492a846 100644 --- a/python_files/get_variable_info.py +++ b/python_files/get_variable_info.py @@ -7,7 +7,7 @@ import locale import sys from importlib.util import find_spec -from typing import NamedTuple +from typing import ClassVar, NamedTuple # this class is from in ptvsd/debugpy tools @@ -52,7 +52,7 @@ class SafeRepr(object): # noqa: UP004 # type, prefix string, suffix string, item prefix string, # item key/value separator, item suffix string - dict_types = [(dict, "{", "}", "", ": ", "")] # noqa: RUF012 + dict_types: ClassVar[list] = [(dict, "{", "}", "", ": ", "")] try: from collections import OrderedDict @@ -521,7 +521,8 @@ def _VSCODE_getVariableDescriptions(): # noqa: N802 "language": "python", } for varName in globals() - if type(globals()[varName]).__name__ not in types_to_exclude and not varName.startswith("__") + if type(globals()[varName]).__name__ not in types_to_exclude + and not varName.startswith("__") ] return json.dumps(variables) From 697b0d0b203636d6d948bf7c50b72b43bfdef740 Mon Sep 17 00:00:00 2001 From: amunger Date: Thu, 12 Sep 2024 13:00:38 -0700 Subject: [PATCH 11/23] avoid named tuple --- python_files/get_variable_info.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/python_files/get_variable_info.py b/python_files/get_variable_info.py index c1cee492a846..e162a69b1479 100644 --- a/python_files/get_variable_info.py +++ b/python_files/get_variable_info.py @@ -7,7 +7,7 @@ import locale import sys from importlib.util import find_spec -from typing import ClassVar, NamedTuple +from typing import ClassVar # this class is from in ptvsd/debugpy tools @@ -398,9 +398,10 @@ def _bytes_as_unicode_if_possible(self, obj_repr): return obj_repr # Return the original version (in bytes) -class DisplayOptions(NamedTuple): - width: int - max_columns: int +class DisplayOptions: + def __init__(self, width, max_columns): + self.width = width + self.max_columns = max_columns safe_repr = SafeRepr() From 5d771eae8b7e16b90ca356b26b7a863571a5e6f6 Mon Sep 17 00:00:00 2001 From: amunger Date: Thu, 12 Sep 2024 13:57:00 -0700 Subject: [PATCH 12/23] more python lint fixes --- python_files/get_variable_info.py | 38 ++----------------------------- 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/python_files/get_variable_info.py b/python_files/get_variable_info.py index e162a69b1479..5dcbc28fe93e 100644 --- a/python_files/get_variable_info.py +++ b/python_files/get_variable_info.py @@ -6,7 +6,6 @@ import json import locale import sys -from importlib.util import find_spec from typing import ClassVar @@ -376,7 +375,7 @@ def _bytes_as_unicode_if_possible(self, obj_repr): # locale.getpreferredencoding() and 'utf-8). If no encoding can decode # the input, we return the original bytes. try_encodings = [] - encoding = self.sys_stdout_encoding or getattr(sys.stdout, "encoding", "") + encoding = self.sys_stdout_encoding or getattr(sys.stdout, "encoding", None) if encoding: try_encodings.append(encoding.lower()) @@ -409,41 +408,8 @@ def __init__(self, width, max_columns): array_page_size = 50 -def set_pandas_display_options(display_options=None): - if find_spec("pandas") is not None: - try: - import pandas as _VSCODE_PD # type: ignore # noqa: N812 - - original_display = DisplayOptions( - width=_VSCODE_PD.options.display.width, - max_columns=_VSCODE_PD.options.display.max_columns, - ) - - if display_options: - _VSCODE_PD.options.display.max_columns = display_options.max_columns - _VSCODE_PD.options.display.width = display_options.width - else: - _VSCODE_PD.options.display.max_columns = 100 - _VSCODE_PD.options.display.width = 1000 - - return original_display - except ImportError: - pass - finally: - del _VSCODE_PD - return None - - def get_value(variable): - original_display = None - if type(variable).__name__ == "DataFrame" and find_spec("pandas") is not None: - original_display = set_pandas_display_options() - - try: - return safe_repr(variable) - finally: - if original_display: - set_pandas_display_options(original_display) + return safe_repr(variable) def get_property_names(variable): From c52e36c382fbf5dbc4b6c1f10af26328af12da87 Mon Sep 17 00:00:00 2001 From: amunger Date: Thu, 12 Sep 2024 14:22:50 -0700 Subject: [PATCH 13/23] set engine to insiders --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 59795d9523c3..7c2bccd50d24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -116,7 +116,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.94.0" + "vscode": "^1.94-insider" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index 5fb6fe4027b3..446178c5d371 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.94.0" + "vscode": "^1.94-insider" }, "enableTelemetry": false, "keywords": [ From c3f2b08b673e64ea7477fc11905ea2405e522d65 Mon Sep 17 00:00:00 2001 From: amunger Date: Thu, 12 Sep 2024 14:27:39 -0700 Subject: [PATCH 14/23] again --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 446178c5d371..58db6339c21e 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.94-insider" + "vscode": "^1.94.0-insider" }, "enableTelemetry": false, "keywords": [ From 5d47db6309bd228b51bce82349a8b8d665392b9e Mon Sep 17 00:00:00 2001 From: amunger Date: Fri, 13 Sep 2024 16:06:35 -0700 Subject: [PATCH 15/23] start testing python script --- python_files/get_variable_info.py | 10 ++-- python_files/tests/test_get_variable_info.py | 50 +++++++++++++++++++ .../repl/variables/variableRequester.ts | 6 +-- 3 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 python_files/tests/test_get_variable_info.py diff --git a/python_files/get_variable_info.py b/python_files/get_variable_info.py index 5dcbc28fe93e..ef8b30085d18 100644 --- a/python_files/get_variable_info.py +++ b/python_files/get_variable_info.py @@ -478,8 +478,8 @@ def get_child_property(root, property_chain): ### Get info on variables at the root level -def _VSCODE_getVariableDescriptions(): # noqa: N802 - variables = [ +def getVariableDescriptions(): # noqa: N802 + return [ { "name": varName, **get_variable_description(globals()[varName]), @@ -492,11 +492,9 @@ def _VSCODE_getVariableDescriptions(): # noqa: N802 and not varName.startswith("__") ] - return json.dumps(variables) - ### Get info on children of a variable reached through the given property chain -def _VSCODE_getAllChildrenDescriptions(root_var_name, property_chain, start_index): # noqa: N802 +def getAllChildrenDescriptions(root_var_name, property_chain, start_index): # noqa: N802 root = globals()[root_var_name] if root is None: return [] @@ -540,4 +538,4 @@ def _VSCODE_getAllChildrenDescriptions(root_var_name, property_chain, start_inde } children.append(child) - return json.dumps(children) + return children diff --git a/python_files/tests/test_get_variable_info.py b/python_files/tests/test_get_variable_info.py new file mode 100644 index 000000000000..891ca774d232 --- /dev/null +++ b/python_files/tests/test_get_variable_info.py @@ -0,0 +1,50 @@ +import get_variable_info + + +def set_global_variable(value): + # setting on the module allows tests to set a variable that the module under test can access + get_variable_info.test_variable = value + + +def get_global_variable(): + results = get_variable_info.getVariableDescriptions() + for variable in results: + if variable["name"] == "test_variable": + return variable + return None + + +def assert_variable_found(variable, expected_value, expected_type, expected_count): + set_global_variable(variable) + variable = get_global_variable() + assert variable["value"] == expected_value + assert variable["type"] == expected_type + if expected_count is not None: + assert variable["count"] == expected_count + return variable + + +def test_simple(): + assert_variable_found(1, "1", "int", None) + + +def test_list(): + assert_variable_found([1, 2, 3], "[1, 2, 3]", "list", None) + + +def test_dict(): + assert_variable_found({"a": 1, "b": 2}, "{'a': 1, 'b': 2}", "dict", None) + + +def test_tuple(): + assert_variable_found((1, 2, 3), "(1, 2, 3)", "tuple", None) + + +def test_set(): + assert_variable_found({1, 2, 3}, "{1, 2, 3}", "set", None) + + +def test_self_referencing_dict(): + d = {} + d["self"] = d + assert_variable_found(d, "{'self': {...}}", "dict", None) diff --git a/src/client/repl/variables/variableRequester.ts b/src/client/repl/variables/variableRequester.ts index 93ddf053274c..e9154c17fbbb 100644 --- a/src/client/repl/variables/variableRequester.ts +++ b/src/client/repl/variables/variableRequester.ts @@ -19,12 +19,12 @@ export class VariableRequester { ): Promise { const scriptLines = (await getContentsOfVariablesScript()).split(/(?:\r\n|\n)/); if (parent) { - const printCall = `return _VSCODE_getAllChildrenDescriptions(\'${parent.root}\', ${JSON.stringify( + const printCall = `return json.dumps(getAllChildrenDescriptions(\'${parent.root}\', ${JSON.stringify( parent.propertyChain, - )}, ${start})`; + )}, ${start}))`; scriptLines.push(printCall); } else { - scriptLines.push('return _VSCODE_getVariableDescriptions()'); + scriptLines.push('return json.dumps(getVariableDescriptions())'); } if (token.isCancellationRequested) { From 1d2de1030229132494f7898ea98b6fa7ab7339b9 Mon Sep 17 00:00:00 2001 From: amunger Date: Mon, 16 Sep 2024 09:36:17 -0700 Subject: [PATCH 16/23] more tests --- python_files/tests/test_get_variable_info.py | 74 ++++++++++++++++++-- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/python_files/tests/test_get_variable_info.py b/python_files/tests/test_get_variable_info.py index 891ca774d232..2b2bb0c61472 100644 --- a/python_files/tests/test_get_variable_info.py +++ b/python_files/tests/test_get_variable_info.py @@ -17,34 +17,96 @@ def get_global_variable(): def assert_variable_found(variable, expected_value, expected_type, expected_count): set_global_variable(variable) variable = get_global_variable() - assert variable["value"] == expected_value + if expected_value is not None: + assert variable["value"] == expected_value assert variable["type"] == expected_type if expected_count is not None: assert variable["count"] == expected_count + else: + assert "count" not in variable return variable +def assert_indexed_child(variable, start_index, expected_index, expected_child_value=None): + children = get_variable_info.getAllChildrenDescriptions( + variable.get("root"), variable.get("propertyChain"), start_index + ) + child = children[expected_index] + + if expected_child_value is not None: + assert child["value"] == expected_child_value + return child + + +def assert_property(variable, expected_property_name, expected_property_value = None): + children = get_variable_info.getAllChildrenDescriptions( + variable.get("root"), variable.get("propertyChain"), 0 + ) + found = None + for child in children: + chain = child.get("propertyChain") + property_name = chain[-1] if chain else None + if property_name == expected_property_name: + found = child + break + + assert found is not None + if expected_property_value is not None: + assert child["value"] == expected_property_value + return child + + def test_simple(): assert_variable_found(1, "1", "int", None) def test_list(): - assert_variable_found([1, 2, 3], "[1, 2, 3]", "list", None) + found = assert_variable_found([1, 2, 3], "[1, 2, 3]", "list", 3) + assert_indexed_child(found, 0, 0, "1") def test_dict(): - assert_variable_found({"a": 1, "b": 2}, "{'a': 1, 'b': 2}", "dict", None) + found = assert_variable_found({"a": 1, "b": 2}, "{'a': 1, 'b': 2}", "dict", None) + assert found["hasNamedChildren"] + assert_property(found, "a", "2") + assert_property(found, "b", "2") def test_tuple(): - assert_variable_found((1, 2, 3), "(1, 2, 3)", "tuple", None) + found = assert_variable_found((1, 2, 3), "(1, 2, 3)", "tuple", 3) + assert_indexed_child(found, 0, 0, "1") def test_set(): - assert_variable_found({1, 2, 3}, "{1, 2, 3}", "set", None) + found = assert_variable_found({1, 2, 3}, "{1, 2, 3}", "set", 3) + assert_indexed_child(found, 0, 0, "1") def test_self_referencing_dict(): d = {} d["self"] = d - assert_variable_found(d, "{'self': {...}}", "dict", None) + found = assert_variable_found(d, "{'self': {...}}", "dict", None) + assert_property(found, "self", "{'self': {...}}") + + +def test_nested_list(): + found = assert_variable_found([[1, 2], [3, 4]], "[[1, 2], [3, 4]]", "list", 2) + assert_indexed_child(found, 0, 0, "[1, 2]") + + +def test_long_list(): + child = assert_variable_found(list(range(1_000_000)), None, "list", 1_000_000) + value = child.get("value") + assert value.startswith("[0, 1, 2, 3") + assert value.endswith("...]") + assert_indexed_child(child, 400_000, 10, "400010") + assert_indexed_child(child, 999_950, 10, "999960") + +def test_get_nested_children(): + d = [{"a": {("hello")}}] + found = assert_variable_found(d, "[{'a': {...}}]", "list", 1) + + found = assert_indexed_child(found, 0, 0) + found = assert_property(found, "a") + found = assert_indexed_child(found, 0, 0) + assert found["value"] == "'hello'" From 98041b5f98ee6066a02c1f689bb861f98b812d54 Mon Sep 17 00:00:00 2001 From: amunger Date: Mon, 16 Sep 2024 12:54:47 -0700 Subject: [PATCH 17/23] cleanup --- python_files/get_variable_info.py | 44 +++++++++---------- .../repl/variables/variableRequester.ts | 8 ++-- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/python_files/get_variable_info.py b/python_files/get_variable_info.py index ef8b30085d18..d60795982617 100644 --- a/python_files/get_variable_info.py +++ b/python_files/get_variable_info.py @@ -2,8 +2,6 @@ # Licensed under the MIT License. See LICENSE in the project root # for license information. -# Gotten from ptvsd for supporting the format expected there. -import json import locale import sys from typing import ClassVar @@ -403,16 +401,16 @@ def __init__(self, width, max_columns): self.max_columns = max_columns -safe_repr = SafeRepr() -collection_types = ["list", "tuple", "set"] -array_page_size = 50 +_safe_repr = SafeRepr() +_collection_types = ["list", "tuple", "set"] +_array_page_size = 50 -def get_value(variable): - return safe_repr(variable) +def _get_value(variable): + return _safe_repr(variable) -def get_property_names(variable): +def _get_property_names(variable): props = [] private_props = [] for prop in dir(variable): @@ -423,7 +421,7 @@ def get_property_names(variable): return props + private_props -def get_full_type(var_type): +def _get_full_type(var_type): module = "" if hasattr(var_type, "__module__") and var_type.__module__ != "builtins": module = var_type.__module__ + "." @@ -434,24 +432,24 @@ def get_full_type(var_type): return None -def get_variable_description(variable): +def _get_variable_description(variable): result = {} var_type = type(variable) - result["type"] = get_full_type(var_type) + result["type"] = _get_full_type(var_type) if hasattr(var_type, "__mro__"): - result["interfaces"] = [get_full_type(t) for t in var_type.__mro__] + result["interfaces"] = [_get_full_type(t) for t in var_type.__mro__] - if hasattr(variable, "__len__") and result["type"] in collection_types: + if hasattr(variable, "__len__") and result["type"] in _collection_types: result["count"] = len(variable) result["hasNamedChildren"] = hasattr(variable, "__dict__") or isinstance(variable, dict) - result["value"] = get_value(variable) + result["value"] = _get_value(variable) return result -def get_child_property(root, property_chain): +def _get_child_property(root, property_chain): try: variable = root for prop in property_chain: @@ -482,7 +480,7 @@ def getVariableDescriptions(): # noqa: N802 return [ { "name": varName, - **get_variable_description(globals()[varName]), + **_get_variable_description(globals()[varName]), "root": varName, "propertyChain": [], "language": "python", @@ -501,17 +499,17 @@ def getAllChildrenDescriptions(root_var_name, property_chain, start_index): # n parent = root if len(property_chain) > 0: - parent = get_child_property(root, property_chain) + parent = _get_child_property(root, property_chain) children = [] - parent_info = get_variable_description(parent) + parent_info = _get_variable_description(parent) if "count" in parent_info: if parent_info["count"] > 0: - last_item = min(parent_info["count"], start_index + array_page_size) + last_item = min(parent_info["count"], start_index + _array_page_size) index_range = range(start_index, last_item) children = [ { - **get_variable_description(get_child_property(parent, [i])), + **_get_variable_description(_get_child_property(parent, [i])), "name": str(i), "root": root_var_name, "propertyChain": [*property_chain, i], @@ -522,16 +520,16 @@ def getAllChildrenDescriptions(root_var_name, property_chain, start_index): # n elif parent_info["hasNamedChildren"]: children_names = [] if hasattr(parent, "__dict__"): - children_names = get_property_names(parent) + children_names = _get_property_names(parent) elif isinstance(parent, dict): children_names = list(parent.keys()) children = [] for prop in children_names: - child_property = get_child_property(parent, [prop]) + child_property = _get_child_property(parent, [prop]) if child_property is not None and type(child_property).__name__ not in types_to_exclude: child = { - **get_variable_description(child_property), + **_get_variable_description(child_property), "name": prop, "root": root_var_name, "propertyChain": [*property_chain, prop], diff --git a/src/client/repl/variables/variableRequester.ts b/src/client/repl/variables/variableRequester.ts index e9154c17fbbb..e66afdcd6616 100644 --- a/src/client/repl/variables/variableRequester.ts +++ b/src/client/repl/variables/variableRequester.ts @@ -19,12 +19,12 @@ export class VariableRequester { ): Promise { const scriptLines = (await getContentsOfVariablesScript()).split(/(?:\r\n|\n)/); if (parent) { - const printCall = `return json.dumps(getAllChildrenDescriptions(\'${parent.root}\', ${JSON.stringify( - parent.propertyChain, - )}, ${start}))`; + const printCall = `import json;return json.dumps(getAllChildrenDescriptions(\'${ + parent.root + }\', ${JSON.stringify(parent.propertyChain)}, ${start}))`; scriptLines.push(printCall); } else { - scriptLines.push('return json.dumps(getVariableDescriptions())'); + scriptLines.push('import json;return json.dumps(getVariableDescriptions())'); } if (token.isCancellationRequested) { From ecd18f0057f0fda6b302b3ecc8b9ca950624f759 Mon Sep 17 00:00:00 2001 From: amunger Date: Mon, 16 Sep 2024 12:59:14 -0700 Subject: [PATCH 18/23] formatting --- python_files/tests/test_get_variable_info.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python_files/tests/test_get_variable_info.py b/python_files/tests/test_get_variable_info.py index 2b2bb0c61472..8c617a216192 100644 --- a/python_files/tests/test_get_variable_info.py +++ b/python_files/tests/test_get_variable_info.py @@ -38,7 +38,7 @@ def assert_indexed_child(variable, start_index, expected_index, expected_child_v return child -def assert_property(variable, expected_property_name, expected_property_value = None): +def assert_property(variable, expected_property_name, expected_property_value=None): children = get_variable_info.getAllChildrenDescriptions( variable.get("root"), variable.get("propertyChain"), 0 ) @@ -102,6 +102,7 @@ def test_long_list(): assert_indexed_child(child, 400_000, 10, "400010") assert_indexed_child(child, 999_950, 10, "999960") + def test_get_nested_children(): d = [{"a": {("hello")}}] found = assert_variable_found(d, "[{'a': {...}}]", "list", 1) From 820728b0a1927573e8721a0001c3168ab8f90610 Mon Sep 17 00:00:00 2001 From: amunger Date: Mon, 16 Sep 2024 13:29:27 -0700 Subject: [PATCH 19/23] finish merge --- package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.json b/package.json index 78cd270910a3..8fe6ab5c57c8 100644 --- a/package.json +++ b/package.json @@ -47,11 +47,7 @@ "theme": "dark" }, "engines": { -<<<<<<< HEAD - "vscode": "^1.94.0-insider" -======= "vscode": "^1.94.0-20240913" ->>>>>>> origin/main }, "enableTelemetry": false, "keywords": [ From 0daf492b85f13ecb8a04b81af41d848bc20f8d90 Mon Sep 17 00:00:00 2001 From: amunger Date: Mon, 16 Sep 2024 13:37:25 -0700 Subject: [PATCH 20/23] fix test --- python_files/tests/test_get_variable_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_files/tests/test_get_variable_info.py b/python_files/tests/test_get_variable_info.py index 8c617a216192..188a9f393288 100644 --- a/python_files/tests/test_get_variable_info.py +++ b/python_files/tests/test_get_variable_info.py @@ -68,7 +68,7 @@ def test_list(): def test_dict(): found = assert_variable_found({"a": 1, "b": 2}, "{'a': 1, 'b': 2}", "dict", None) assert found["hasNamedChildren"] - assert_property(found, "a", "2") + assert_property(found, "a", "1") assert_property(found, "b", "2") From e47abb9fc938a853a952a31ff9f04de5a899c4e1 Mon Sep 17 00:00:00 2001 From: amunger Date: Mon, 16 Sep 2024 14:21:44 -0700 Subject: [PATCH 21/23] fixes? --- python_files/tests/test_get_variable_info.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/python_files/tests/test_get_variable_info.py b/python_files/tests/test_get_variable_info.py index 188a9f393288..db0f62b8f137 100644 --- a/python_files/tests/test_get_variable_info.py +++ b/python_files/tests/test_get_variable_info.py @@ -3,7 +3,7 @@ def set_global_variable(value): # setting on the module allows tests to set a variable that the module under test can access - get_variable_info.test_variable = value + get_variable_info.test_variable = value # pyright: ignore[reportGeneralTypeIssues] def get_global_variable(): @@ -14,7 +14,7 @@ def get_global_variable(): return None -def assert_variable_found(variable, expected_value, expected_type, expected_count): +def assert_variable_found(variable, expected_value, expected_type, expected_count=None): set_global_variable(variable) variable = get_global_variable() if expected_value is not None: @@ -29,7 +29,7 @@ def assert_variable_found(variable, expected_value, expected_type, expected_coun def assert_indexed_child(variable, start_index, expected_index, expected_child_value=None): children = get_variable_info.getAllChildrenDescriptions( - variable.get("root"), variable.get("propertyChain"), start_index + variable["root"], variable["propertyChain"], start_index ) child = children[expected_index] @@ -40,11 +40,11 @@ def assert_indexed_child(variable, start_index, expected_index, expected_child_v def assert_property(variable, expected_property_name, expected_property_value=None): children = get_variable_info.getAllChildrenDescriptions( - variable.get("root"), variable.get("propertyChain"), 0 + variable["root"], variable["propertyChain"], 0 ) found = None for child in children: - chain = child.get("propertyChain") + chain = child["propertyChain"] property_name = chain[-1] if chain else None if property_name == expected_property_name: found = child @@ -67,6 +67,7 @@ def test_list(): def test_dict(): found = assert_variable_found({"a": 1, "b": 2}, "{'a': 1, 'b': 2}", "dict", None) + assert found is not None assert found["hasNamedChildren"] assert_property(found, "a", "1") assert_property(found, "b", "2") @@ -96,7 +97,7 @@ def test_nested_list(): def test_long_list(): child = assert_variable_found(list(range(1_000_000)), None, "list", 1_000_000) - value = child.get("value") + value = child["value"] assert value.startswith("[0, 1, 2, 3") assert value.endswith("...]") assert_indexed_child(child, 400_000, 10, "400010") From 6bd2353abd43635f0806dd1bd0b7ba749b20d628 Mon Sep 17 00:00:00 2001 From: amunger Date: Mon, 16 Sep 2024 14:35:49 -0700 Subject: [PATCH 22/23] type fixes --- python_files/tests/test_get_variable_info.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python_files/tests/test_get_variable_info.py b/python_files/tests/test_get_variable_info.py index db0f62b8f137..73f94fe26f06 100644 --- a/python_files/tests/test_get_variable_info.py +++ b/python_files/tests/test_get_variable_info.py @@ -17,6 +17,7 @@ def get_global_variable(): def assert_variable_found(variable, expected_value, expected_type, expected_count=None): set_global_variable(variable) variable = get_global_variable() + assert variable is not None if expected_value is not None: assert variable["value"] == expected_value assert variable["type"] == expected_type @@ -52,8 +53,8 @@ def assert_property(variable, expected_property_name, expected_property_value=No assert found is not None if expected_property_value is not None: - assert child["value"] == expected_property_value - return child + assert found["value"] == expected_property_value + return found def test_simple(): @@ -67,7 +68,6 @@ def test_list(): def test_dict(): found = assert_variable_found({"a": 1, "b": 2}, "{'a': 1, 'b': 2}", "dict", None) - assert found is not None assert found["hasNamedChildren"] assert_property(found, "a", "1") assert_property(found, "b", "2") From 788bec6f3980d1512d8e5f6c689692226bf0f3b9 Mon Sep 17 00:00:00 2001 From: amunger Date: Tue, 17 Sep 2024 08:59:50 -0700 Subject: [PATCH 23/23] revert settings files --- src/test/.vscode/settings.json | 3 ++- src/testMultiRootWkspc/multi.code-workspace | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json index faeb48ffa29c..cd2b4152591d 100644 --- a/src/test/.vscode/settings.json +++ b/src/test/.vscode/settings.json @@ -13,5 +13,6 @@ "python.linting.banditEnabled": false, // Don't set this to `Pylance`, for CI we want to use the LS that ships with the extension. "python.languageServer": "Jedi", - "python.pythonPath": "C:\\GIT\\s p\\vscode-python\\.venv\\Scripts\\python.exe" + "python.pythonPath": "C:\\GIT\\s p\\vscode-python\\.venv\\Scripts\\python.exe", + "python.defaultInterpreterPath": "python" } diff --git a/src/testMultiRootWkspc/multi.code-workspace b/src/testMultiRootWkspc/multi.code-workspace index 5ed4fa94902f..51d218783041 100644 --- a/src/testMultiRootWkspc/multi.code-workspace +++ b/src/testMultiRootWkspc/multi.code-workspace @@ -39,7 +39,6 @@ "python.linting.prospectorEnabled": false, "python.linting.lintOnSave": false, "python.linting.enabled": true, - "python.pythonPath": "python", - "python.defaultInterpreterPath": "python" + "python.pythonPath": "python" } }