Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Interactive REPL Experiment #23235

Merged
merged 40 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
51b5f8e
start adding interactive REPL experiment
anthonykim1 Apr 15, 2024
51278d5
add execInREPL command
anthonykim1 Apr 15, 2024
8dbbd05
add REPL command
anthonykim1 Apr 15, 2024
5b76e4a
replCommands, replController
anthonykim1 Apr 15, 2024
e92c539
check in current status
anthonykim1 Apr 16, 2024
bc15f8f
myChanges
anthonykim1 Apr 16, 2024
200ca1a
big progress
anthonykim1 Apr 16, 2024
416ea96
save notebook editor
anthonykim1 Apr 16, 2024
71d3e22
start getting user input to pass to IW
anthonykim1 Apr 17, 2024
a2208ca
user input feeds into IW
anthonykim1 Apr 17, 2024
c8a9b35
Remove comment, add TODO, cleanup
anthonykim1 Apr 17, 2024
66515c6
rename variables
anthonykim1 Apr 17, 2024
2fede16
save comment of TODO
anthonykim1 May 1, 2024
0f1488c
make Run REPL work again
anthonykim1 May 6, 2024
c3a3028
save progress 05/06
anthonykim1 May 7, 2024
293a476
fix typescript test
anthonykim1 May 7, 2024
3b38072
typescript please
anthonykim1 May 7, 2024
e248afd
fix python error
anthonykim1 May 7, 2024
f329dd1
comment debugpy
anthonykim1 May 7, 2024
0bb23cf
stop modifying settings.json
anthonykim1 May 7, 2024
19cee66
try to fix lint again
anthonykim1 May 7, 2024
a4f7783
stop modifying setttings.json
anthonykim1 May 7, 2024
7258a5f
fix lint via name=''
anthonykim1 May 7, 2024
b6a8229
format via prettier
anthonykim1 May 7, 2024
9f5deae
MAKE INTERRUPT WORK
anthonykim1 May 7, 2024
03062bd
get rid of _buffer.name = name
anthonykim1 May 7, 2024
bc9637c
show invalid interpreter when no interpreter is selected
anthonykim1 May 8, 2024
fa76d00
wrap around experiment, context key
anthonykim1 May 9, 2024
6ff2dce
clean up
anthonykim1 May 10, 2024
b8e8fb0
fixing merge conflict
anthonykim1 May 10, 2024
ed65b3a
Merge branch 'main' into RunWithREPL
anthonykim1 May 10, 2024
1a378bb
prettier remove blank space
anthonykim1 May 10, 2024
a11c1c7
cleanup extensionActivation.ts
anthonykim1 May 10, 2024
d9ba156
more cleanup
anthonykim1 May 10, 2024
ea55d8b
update current status, start cleanup
anthonykim1 May 15, 2024
da93090
remove overriding read()
anthonykim1 May 15, 2024
c7f5372
fix lint
anthonykim1 May 15, 2024
289edb6
more cleanup
anthonykim1 May 16, 2024
19e73b1
more cleanup
anthonykim1 May 16, 2024
03d6ed3
await workspace edit from feedback
anthonykim1 May 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@
],
"compounds": [
{
"name": "Debug Test Discovery",
"name": "Debug Python and Extension",
anthonykim1 marked this conversation as resolved.
Show resolved Hide resolved
"configurations": ["Python: Attach Listen", "Extension"]
}
]
Expand Down
28 changes: 24 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,11 @@
"command": "python.execSelectionInTerminal",
"title": "%python.command.python.execSelectionInTerminal.title%"
},
{
"category": "Python",
"command": "python.execInREPL",
"title": "%python.command.python.execInREPL.title%"
},
{
"category": "Python",
"command": "python.launchTensorBoard",
Expand Down Expand Up @@ -437,7 +442,8 @@
"pythonDiscoveryUsingWorkers",
"pythonTestAdapter",
"pythonREPLSmartSend",
"pythonRecommendTensorboardExt"
"pythonRecommendTensorboardExt",
"pythonRunREPL"
],
"enumDescriptions": [
"%python.experiments.All.description%",
Expand All @@ -447,7 +453,8 @@
"%python.experiments.pythonDiscoveryUsingWorkers.description%",
"%python.experiments.pythonTestAdapter.description%",
"%python.experiments.pythonREPLSmartSend.description%",
"%python.experiments.pythonRecommendTensorboardExt.description%"
"%python.experiments.pythonRecommendTensorboardExt.description%",
"%python.experiments.pythonRunREPL.description%"
]
},
"scope": "window",
Expand All @@ -465,7 +472,8 @@
"pythonTerminalEnvVarActivation",
"pythonDiscoveryUsingWorkers",
"pythonTestAdapter",
"pythonREPLSmartSend"
"pythonREPLSmartSend",
"pythonRunREPL"
],
"enumDescriptions": [
"%python.experiments.All.description%",
Expand All @@ -474,7 +482,8 @@
"%python.experiments.pythonTerminalEnvVarActivation.description%",
"%python.experiments.pythonDiscoveryUsingWorkers.description%",
"%python.experiments.pythonTestAdapter.description%",
"%python.experiments.pythonREPLSmartSend.description%"
"%python.experiments.pythonREPLSmartSend.description%",
"%python.experiments.pythonRunREPL.description%"
]
},
"scope": "window",
Expand Down Expand Up @@ -1241,6 +1250,12 @@
"title": "%python.command.python.execSelectionInTerminal.title%",
"when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python"
},
{
"category": "Python",
"command": "python.execInREPL",
"title": "%python.command.python.execInREPL.title%",
"when": "false"
},
{
"category": "Python",
"command": "python.launchTensorBoard",
Expand Down Expand Up @@ -1340,6 +1355,11 @@
"command": "python.execSelectionInTerminal",
"group": "Python",
"when": "editorFocus && editorLangId == python && !virtualWorkspace && shellExecutionSupported"
},
{
"command": "python.execInREPL",
"group": "Python",
"when": "editorFocus && editorLangId == python && !virtualWorkspace && shellExecutionSupported && pythonRunREPL"
}
],
"editor/title": [
Expand Down
4 changes: 3 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"python.command.python.configureTests.title": "Configure Tests",
"python.command.testing.rerunFailedTests.title": "Rerun Failed Tests",
"python.command.python.execSelectionInTerminal.title": "Run Selection/Line in Python Terminal",
"python.command.python.execInREPL.title": "Run Selection/Line in Python REPL",
"python.command.python.execSelectionInDjangoShell.title": "Run Selection/Line in Django Shell",
"python.command.python.reportIssue.title": "Report Issue...",
"python.command.python.enableSourceMapSupport.title": "Enable Source Map Support For Extension Debugging",
Expand Down Expand Up @@ -44,6 +45,7 @@
"python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.",
"python.experiments.pythonREPLSmartSend.description": "Denotes the Python REPL Smart Send experiment.",
"python.experiments.pythonRecommendTensorboardExt.description": "Denotes the Tensorboard Extension recommendation experiment.",
"python.experiments.pythonRunREPL.description": "Enables users to run code in interactive Python REPL.",
"python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.",
"python.languageServer.description": "Defines type of the language server.",
"python.languageServer.defaultDescription": "Automatically select a language server: Pylance if installed and available, otherwise fallback to Jedi.",
Expand Down Expand Up @@ -162,4 +164,4 @@
"walkthrough.step.python.createNewNotebook.altText": "Creating a new Jupyter notebook",
"walkthrough.step.python.openInteractiveWindow.altText": "Opening Python interactive window",
"walkthrough.step.python.dataScienceLearnMore.altText": "Image representing our documentation page and mailing list resources."
}
}
167 changes: 167 additions & 0 deletions python_files/python_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
from typing import Dict, List, Optional, Union

import sys
import json
import contextlib
import io
import traceback
import uuid

STDIN = sys.stdin
STDOUT = sys.stdout
STDERR = sys.stderr
USER_GLOBALS = {}


def send_message(msg: str):
length_msg = len(msg)
STDOUT.buffer.write(f"Content-Length: {length_msg}\r\n\r\n{msg}".encode(encoding="utf-8"))
STDOUT.buffer.flush()


def print_log(msg: str):
send_message(json.dumps({"jsonrpc": "2.0", "method": "log", "params": msg}))


def send_response(response: str, response_id: int):
send_message(json.dumps({"jsonrpc": "2.0", "id": response_id, "result": response}))


def send_request(params: Optional[Union[List, Dict]] = None):
request_id = uuid.uuid4().hex
if params is None:
send_message(json.dumps({"jsonrpc": "2.0", "id": request_id, "method": "input"}))
else:
send_message(
json.dumps({"jsonrpc": "2.0", "id": request_id, "method": "input", "params": params})
)
return request_id


original_input = input


def custom_input(prompt=""):
try:
send_request({"prompt": prompt})
headers = get_headers()
content_length = int(headers.get("Content-Length", 0))

if content_length:
message_text = STDIN.read(content_length)
message_json = json.loads(message_text)
our_user_input = message_json["result"]["userInput"]
return our_user_input
except Exception:
print_log(traceback.format_exc())


# Set input to our custom input
USER_GLOBALS["input"] = custom_input
input = custom_input


def handle_response(request_id):
while not STDIN.closed:
try:
headers = get_headers()
content_length = int(headers.get("Content-Length", 0))

if content_length:
message_text = STDIN.read(content_length)
message_json = json.loads(message_text)
our_user_input = message_json["result"]["userInput"]
if message_json["id"] == request_id:
send_response(our_user_input, message_json["id"])
elif message_json["method"] == "exit":
sys.exit(0)

except Exception:
print_log(traceback.format_exc())


def exec_function(user_input):
try:
compile(user_input, "<stdin>", "eval")
except SyntaxError:
return exec
return eval


def execute(request, user_globals):
str_output = CustomIO("<stdout>", encoding="utf-8")
str_error = CustomIO("<stderr>", encoding="utf-8")

with redirect_io("stdout", str_output):
with redirect_io("stderr", str_error):
str_input = CustomIO("<stdin>", encoding="utf-8", newline="\n")
with redirect_io("stdin", str_input):
exec_user_input(request["params"], user_globals)
send_response(str_output.get_value(), request["id"])


def exec_user_input(user_input, user_globals):
user_input = user_input[0] if isinstance(user_input, list) else user_input

try:
callable = exec_function(user_input)
retval = callable(user_input, user_globals)
if retval is not None:
print(retval)
except KeyboardInterrupt:
print(traceback.format_exc())
except Exception:
print(traceback.format_exc())


class CustomIO(io.TextIOWrapper):
"""Custom stream object to replace stdio."""

def __init__(self, name, encoding="utf-8", newline=None):
self._buffer = io.BytesIO()
self._custom_name = name
super().__init__(self._buffer, encoding=encoding, newline=newline)

def close(self):
"""Provide this close method which is used by some tools."""
# This is intentionally empty.

def get_value(self) -> str:
"""Returns value from the buffer as string."""
self.seek(0)
return self.read()


@contextlib.contextmanager
def redirect_io(stream: str, new_stream):
"""Redirect stdio streams to a custom stream."""
old_stream = getattr(sys, stream)
setattr(sys, stream, new_stream)
yield
setattr(sys, stream, old_stream)


def get_headers():
headers = {}
while line := STDIN.readline().strip():
name, value = line.split(":", 1)
headers[name] = value.strip()
return headers


if __name__ == "__main__":
while not STDIN.closed:
try:
headers = get_headers()
content_length = int(headers.get("Content-Length", 0))

if content_length:
request_text = STDIN.read(content_length)
request_json = json.loads(request_text)
if request_json["method"] == "execute":
execute(request_json, USER_GLOBALS)
elif request_json["method"] == "exit":
sys.exit(0)

except Exception:
print_log(traceback.format_exc())
1 change: 1 addition & 0 deletions src/client/common/application/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface ICommandNameWithoutArgumentTypeMapping {
[Commands.Enable_SourceMap_Support]: [];
[Commands.Exec_Selection_In_Terminal]: [];
[Commands.Exec_Selection_In_Django_Shell]: [];
[Commands.Exec_In_REPL]: [];
[Commands.Create_Terminal]: [];
[Commands.PickLocalProcess]: [];
[Commands.ClearStorage]: [];
Expand Down
1 change: 1 addition & 0 deletions src/client/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export namespace Commands {
export const Exec_In_Terminal = 'python.execInTerminal';
export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon';
export const Exec_In_Separate_Terminal = 'python.execInDedicatedTerminal';
export const Exec_In_REPL = 'python.execInREPL';
export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell';
export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal';
export const GetSelectedInterpreterPath = 'python.interpreterPath';
Expand Down
5 changes: 5 additions & 0 deletions src/client/common/experiments/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,8 @@ export enum RecommendTensobardExtension {
export enum CreateEnvOnPipInstallTrigger {
experiment = 'pythonCreateEnvOnPipInstall',
}

// Experiment to enable running Python REPL using IW.
export enum EnableRunREPL {
experiment = 'pythonRunREPL',
}
1 change: 1 addition & 0 deletions src/client/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export interface ITerminalSettings {

export interface IREPLSettings {
readonly enableREPLSmartSend: boolean;
readonly enableIWREPL: boolean;
}

export interface IExperiments {
Expand Down
16 changes: 15 additions & 1 deletion src/client/extensionActivation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

'use strict';

import { DebugConfigurationProvider, debug, languages, window } from 'vscode';
import { DebugConfigurationProvider, debug, languages, window, commands } from 'vscode';

import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry';
import { IExtensionActivationManager } from './activation/types';
Expand All @@ -16,6 +16,7 @@ import { IFileSystem } from './common/platform/types';
import {
IConfigurationService,
IDisposableRegistry,
IExperimentService,
IExtensions,
IInterpreterPathService,
ILogOutputChannel,
Expand Down Expand Up @@ -52,6 +53,8 @@ import { initializePersistentStateForTriggers } from './common/persistentState';
import { logAndNotifyOnLegacySettings } from './logging/settingLogs';
import { DebuggerTypeName } from './debugger/constants';
import { StopWatch } from './common/utils/stopWatch';
import { registerReplCommands } from './repl/replCommands';
import { EnableRunREPL } from './common/experiments/groups';

export async function activateComponents(
// `ext` is passed to any extra activation funcs.
Expand Down Expand Up @@ -105,6 +108,17 @@ export function activateFeatures(ext: ExtensionState, _components: Components):
interpreterService,
pathUtils,
);

// Register native REPL context menu when in experiment
const experimentService = ext.legacyIOC.serviceContainer.get<IExperimentService>(IExperimentService);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

legacy IOC? is there a newer one to use?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually in other files in Python extension if we were to use serviceContainer, we access it via IServiceContainer, which we still could I believe if we register the REPL command in other file? I just thought extensionActivation was the best place to register command and read experiment service since it seemed like the fastest/prioritized when extension loads.

commands.executeCommand('setContext', 'pythonRunREPL', false);
if (experimentService) {
const replExperimentValue = experimentService.inExperimentSync(EnableRunREPL.experiment);
if (replExperimentValue) {
registerReplCommands(ext.disposables, interpreterService);
commands.executeCommand('setContext', 'pythonRunREPL', true);
}
}
}

/// //////////////////////////
Expand Down
Loading
Loading