mirror of
https://github.com/denoland/deno.git
synced 2024-11-24 15:19:26 -05:00
feat(jupyter): support confirm
and prompt
in notebooks (#23592)
Closes: https://github.com/denoland/deno/issues/22633 This commit adds support for `confirm` and `prompt` APIs, that instead of reading from stdin are using notebook frontend to show modal boxes and wait for answers. --------- Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
parent
96b527b8df
commit
f00f0f9298
5 changed files with 165 additions and 13 deletions
|
@ -337,7 +337,14 @@ async function formatInner(obj, raw) {
|
||||||
internals.jupyter = { formatInner };
|
internals.jupyter = { formatInner };
|
||||||
|
|
||||||
function enableJupyter() {
|
function enableJupyter() {
|
||||||
const { op_jupyter_broadcast } = core.ops;
|
const { op_jupyter_broadcast, op_jupyter_input } = core.ops;
|
||||||
|
|
||||||
|
function input(
|
||||||
|
prompt,
|
||||||
|
password,
|
||||||
|
) {
|
||||||
|
return op_jupyter_input(prompt, password);
|
||||||
|
}
|
||||||
|
|
||||||
async function broadcast(
|
async function broadcast(
|
||||||
msgType,
|
msgType,
|
||||||
|
@ -412,6 +419,45 @@ function enableJupyter() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt for user confirmation (in Jupyter Notebook context)
|
||||||
|
* Override confirm and prompt because they depend on a tty
|
||||||
|
* and in the Deno.jupyter environment that doesn't exist.
|
||||||
|
* @param {string} message - The message to display.
|
||||||
|
* @returns {Promise<boolean>} User confirmation.
|
||||||
|
*/
|
||||||
|
function confirm(message = "Confirm") {
|
||||||
|
const answer = input(`${message} [y/N] `, false);
|
||||||
|
return answer === "Y" || answer === "y";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt for user input (in Jupyter Notebook context)
|
||||||
|
* @param {string} message - The message to display.
|
||||||
|
* @param {string} defaultValue - The value used if none is provided.
|
||||||
|
* @param {object} options Options
|
||||||
|
* @param {boolean} options.password Hide the output characters
|
||||||
|
* @returns {Promise<string>} The user input.
|
||||||
|
*/
|
||||||
|
function prompt(
|
||||||
|
message = "Prompt",
|
||||||
|
defaultValue = "",
|
||||||
|
{ password = false } = {},
|
||||||
|
) {
|
||||||
|
if (defaultValue != "") {
|
||||||
|
message += ` [${defaultValue}]`;
|
||||||
|
}
|
||||||
|
const answer = input(`${message}`, password);
|
||||||
|
|
||||||
|
if (answer === "") {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.confirm = confirm;
|
||||||
|
globalThis.prompt = prompt;
|
||||||
globalThis.Deno.jupyter = {
|
globalThis.Deno.jupyter = {
|
||||||
broadcast,
|
broadcast,
|
||||||
display,
|
display,
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
// NOTE(bartlomieju): unfortunately it appears that clippy is broken
|
||||||
|
// and can't allow a single line ignore for `await_holding_lock`.
|
||||||
|
#![allow(clippy::await_holding_lock)]
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use jupyter_runtime::InputRequest;
|
||||||
use jupyter_runtime::JupyterMessage;
|
use jupyter_runtime::JupyterMessage;
|
||||||
use jupyter_runtime::JupyterMessageContent;
|
use jupyter_runtime::JupyterMessageContent;
|
||||||
use jupyter_runtime::KernelIoPubConnection;
|
use jupyter_runtime::KernelIoPubConnection;
|
||||||
|
@ -11,14 +16,17 @@ use jupyter_runtime::StreamContent;
|
||||||
|
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use deno_core::op2;
|
use deno_core::op2;
|
||||||
|
use deno_core::parking_lot::Mutex;
|
||||||
use deno_core::serde_json;
|
use deno_core::serde_json;
|
||||||
use deno_core::OpState;
|
use deno_core::OpState;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
use crate::tools::jupyter::server::StdinConnectionProxy;
|
||||||
|
|
||||||
deno_core::extension!(deno_jupyter,
|
deno_core::extension!(deno_jupyter,
|
||||||
ops = [
|
ops = [
|
||||||
op_jupyter_broadcast,
|
op_jupyter_broadcast,
|
||||||
|
op_jupyter_input,
|
||||||
],
|
],
|
||||||
options = {
|
options = {
|
||||||
sender: mpsc::UnboundedSender<StreamContent>,
|
sender: mpsc::UnboundedSender<StreamContent>,
|
||||||
|
@ -32,6 +40,63 @@ deno_core::extension!(deno_jupyter,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[op2]
|
||||||
|
#[string]
|
||||||
|
pub fn op_jupyter_input(
|
||||||
|
state: &mut OpState,
|
||||||
|
#[string] prompt: String,
|
||||||
|
is_password: bool,
|
||||||
|
) -> Result<Option<String>, AnyError> {
|
||||||
|
let (last_execution_request, stdin_connection_proxy) = {
|
||||||
|
(
|
||||||
|
state.borrow::<Arc<Mutex<Option<JupyterMessage>>>>().clone(),
|
||||||
|
state.borrow::<Arc<Mutex<StdinConnectionProxy>>>().clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let maybe_last_request = last_execution_request.lock().clone();
|
||||||
|
if let Some(last_request) = maybe_last_request {
|
||||||
|
let JupyterMessageContent::ExecuteRequest(msg) = &last_request.content
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
if !msg.allow_stdin {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = JupyterMessage::new(
|
||||||
|
InputRequest {
|
||||||
|
prompt,
|
||||||
|
password: is_password,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Some(&last_request),
|
||||||
|
);
|
||||||
|
|
||||||
|
let Ok(()) = stdin_connection_proxy.lock().tx.send(msg) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Need to spawn a separate thread here, because `blocking_recv()` can't
|
||||||
|
// be used from the Tokio runtime context.
|
||||||
|
let join_handle = std::thread::spawn(move || {
|
||||||
|
stdin_connection_proxy.lock().rx.blocking_recv()
|
||||||
|
});
|
||||||
|
let Ok(Some(response)) = join_handle.join() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let JupyterMessageContent::InputReply(msg) = response.content else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(Some(msg.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
#[op2(async)]
|
#[op2(async)]
|
||||||
pub async fn op_jupyter_broadcast(
|
pub async fn op_jupyter_broadcast(
|
||||||
state: Rc<RefCell<OpState>>,
|
state: Rc<RefCell<OpState>>,
|
||||||
|
@ -49,7 +114,7 @@ pub async fn op_jupyter_broadcast(
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let maybe_last_request = last_execution_request.lock().await.clone();
|
let maybe_last_request = last_execution_request.lock().clone();
|
||||||
if let Some(last_request) = maybe_last_request {
|
if let Some(last_request) = maybe_last_request {
|
||||||
let content = JupyterMessageContent::from_type_and_content(
|
let content = JupyterMessageContent::from_type_and_content(
|
||||||
&message_type,
|
&message_type,
|
||||||
|
@ -69,9 +134,7 @@ pub async fn op_jupyter_broadcast(
|
||||||
.with_metadata(metadata)
|
.with_metadata(metadata)
|
||||||
.with_buffers(buffers.into_iter().map(|b| b.to_vec().into()).collect());
|
.with_buffers(buffers.into_iter().map(|b| b.to_vec().into()).collect());
|
||||||
|
|
||||||
(iopub_connection.lock().await)
|
iopub_connection.lock().send(jupyter_message).await?;
|
||||||
.send(jupyter_message)
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -179,6 +179,7 @@ pub async fn kernel(
|
||||||
let mut op_state = op_state_rc.borrow_mut();
|
let mut op_state = op_state_rc.borrow_mut();
|
||||||
op_state.put(startup_data.iopub_connection.clone());
|
op_state.put(startup_data.iopub_connection.clone());
|
||||||
op_state.put(startup_data.last_execution_request.clone());
|
op_state.put(startup_data.last_execution_request.clone());
|
||||||
|
op_state.put(startup_data.stdin_connection_proxy.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
repl_session_proxy.start().await;
|
repl_session_proxy.start().await;
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
// This file is forked/ported from <https://github.com/evcxr/evcxr>
|
// This file is forked/ported from <https://github.com/evcxr/evcxr>
|
||||||
// Copyright 2020 The Evcxr Authors. MIT license.
|
// Copyright 2020 The Evcxr Authors. MIT license.
|
||||||
|
|
||||||
|
// NOTE(bartlomieju): unfortunately it appears that clippy is broken
|
||||||
|
// and can't allow a single line ignore for `await_holding_lock`.
|
||||||
|
#![allow(clippy::await_holding_lock)]
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -12,12 +16,12 @@ use crate::tools::repl;
|
||||||
use deno_core::anyhow::bail;
|
use deno_core::anyhow::bail;
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use deno_core::futures;
|
use deno_core::futures;
|
||||||
|
use deno_core::parking_lot::Mutex;
|
||||||
use deno_core::serde_json;
|
use deno_core::serde_json;
|
||||||
use deno_core::CancelFuture;
|
use deno_core::CancelFuture;
|
||||||
use deno_core::CancelHandle;
|
use deno_core::CancelHandle;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
use jupyter_runtime::messaging;
|
use jupyter_runtime::messaging;
|
||||||
use jupyter_runtime::AsChildOf;
|
use jupyter_runtime::AsChildOf;
|
||||||
|
@ -40,8 +44,14 @@ pub struct JupyterServer {
|
||||||
repl_session_proxy: JupyterReplProxy,
|
repl_session_proxy: JupyterReplProxy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct StdinConnectionProxy {
|
||||||
|
pub tx: mpsc::UnboundedSender<JupyterMessage>,
|
||||||
|
pub rx: mpsc::UnboundedReceiver<JupyterMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct StartupData {
|
pub struct StartupData {
|
||||||
pub iopub_connection: Arc<Mutex<KernelIoPubConnection>>,
|
pub iopub_connection: Arc<Mutex<KernelIoPubConnection>>,
|
||||||
|
pub stdin_connection_proxy: Arc<Mutex<StdinConnectionProxy>>,
|
||||||
pub last_execution_request: Arc<Mutex<Option<JupyterMessage>>>,
|
pub last_execution_request: Arc<Mutex<Option<JupyterMessage>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +68,7 @@ impl JupyterServer {
|
||||||
connection_info.create_kernel_shell_connection().await?;
|
connection_info.create_kernel_shell_connection().await?;
|
||||||
let control_connection =
|
let control_connection =
|
||||||
connection_info.create_kernel_control_connection().await?;
|
connection_info.create_kernel_control_connection().await?;
|
||||||
let _stdin_connection =
|
let mut stdin_connection =
|
||||||
connection_info.create_kernel_stdin_connection().await?;
|
connection_info.create_kernel_stdin_connection().await?;
|
||||||
let iopub_connection =
|
let iopub_connection =
|
||||||
connection_info.create_kernel_iopub_connection().await?;
|
connection_info.create_kernel_iopub_connection().await?;
|
||||||
|
@ -66,9 +76,19 @@ impl JupyterServer {
|
||||||
let iopub_connection = Arc::new(Mutex::new(iopub_connection));
|
let iopub_connection = Arc::new(Mutex::new(iopub_connection));
|
||||||
let last_execution_request = Arc::new(Mutex::new(None));
|
let last_execution_request = Arc::new(Mutex::new(None));
|
||||||
|
|
||||||
|
let (stdin_tx1, mut stdin_rx1) =
|
||||||
|
mpsc::unbounded_channel::<JupyterMessage>();
|
||||||
|
let (stdin_tx2, stdin_rx2) = mpsc::unbounded_channel::<JupyterMessage>();
|
||||||
|
|
||||||
|
let stdin_connection_proxy = Arc::new(Mutex::new(StdinConnectionProxy {
|
||||||
|
tx: stdin_tx1,
|
||||||
|
rx: stdin_rx2,
|
||||||
|
}));
|
||||||
|
|
||||||
let Ok(()) = setup_tx.send(StartupData {
|
let Ok(()) = setup_tx.send(StartupData {
|
||||||
iopub_connection: iopub_connection.clone(),
|
iopub_connection: iopub_connection.clone(),
|
||||||
last_execution_request: last_execution_request.clone(),
|
last_execution_request: last_execution_request.clone(),
|
||||||
|
stdin_connection_proxy,
|
||||||
}) else {
|
}) else {
|
||||||
bail!("Failed to send startup data");
|
bail!("Failed to send startup data");
|
||||||
};
|
};
|
||||||
|
@ -82,6 +102,24 @@ impl JupyterServer {
|
||||||
repl_session_proxy,
|
repl_session_proxy,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let stdin_fut = deno_core::unsync::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let Some(msg) = stdin_rx1.recv().await else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(()) = stdin_connection.send(msg).await else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(msg) = stdin_connection.read().await else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(()) = stdin_tx2.send(msg) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let hearbeat_fut = deno_core::unsync::spawn(async move {
|
let hearbeat_fut = deno_core::unsync::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
if let Err(err) = heartbeat.single_heartbeat().await {
|
if let Err(err) = heartbeat.single_heartbeat().await {
|
||||||
|
@ -134,6 +172,7 @@ impl JupyterServer {
|
||||||
shell_fut,
|
shell_fut,
|
||||||
stdio_fut,
|
stdio_fut,
|
||||||
repl_session_fut,
|
repl_session_fut,
|
||||||
|
stdin_fut,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if let Ok(result) = join_fut.or_cancel(cancel_handle).await {
|
if let Ok(result) = join_fut.or_cancel(cancel_handle).await {
|
||||||
|
@ -148,13 +187,15 @@ impl JupyterServer {
|
||||||
last_execution_request: Arc<Mutex<Option<JupyterMessage>>>,
|
last_execution_request: Arc<Mutex<Option<JupyterMessage>>>,
|
||||||
stdio_msg: StreamContent,
|
stdio_msg: StreamContent,
|
||||||
) {
|
) {
|
||||||
let maybe_exec_result = last_execution_request.lock().await.clone();
|
let maybe_exec_result = last_execution_request.lock().clone();
|
||||||
let Some(exec_request) = maybe_exec_result else {
|
let Some(exec_request) = maybe_exec_result else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut iopub_conn = iopub_connection.lock().await;
|
let result = iopub_connection
|
||||||
let result = iopub_conn.send(stdio_msg.as_child_of(&exec_request)).await;
|
.lock()
|
||||||
|
.send(stdio_msg.as_child_of(&exec_request))
|
||||||
|
.await;
|
||||||
|
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
log::error!("Output error: {}", err);
|
log::error!("Output error: {}", err);
|
||||||
|
@ -429,7 +470,7 @@ impl JupyterServer {
|
||||||
if !execute_request.silent && execute_request.store_history {
|
if !execute_request.silent && execute_request.store_history {
|
||||||
self.execution_count += 1;
|
self.execution_count += 1;
|
||||||
}
|
}
|
||||||
*self.last_execution_request.lock().await = Some(parent_message.clone());
|
*self.last_execution_request.lock() = Some(parent_message.clone());
|
||||||
|
|
||||||
self
|
self
|
||||||
.send_iopub(
|
.send_iopub(
|
||||||
|
@ -613,7 +654,7 @@ impl JupyterServer {
|
||||||
&mut self,
|
&mut self,
|
||||||
message: JupyterMessage,
|
message: JupyterMessage,
|
||||||
) -> Result<(), AnyError> {
|
) -> Result<(), AnyError> {
|
||||||
self.iopub_connection.lock().await.send(message).await
|
self.iopub_connection.lock().send(message).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -579,6 +579,7 @@ const NOT_IMPORTED_OPS = [
|
||||||
|
|
||||||
// Related to `Deno.jupyter` API
|
// Related to `Deno.jupyter` API
|
||||||
"op_jupyter_broadcast",
|
"op_jupyter_broadcast",
|
||||||
|
"op_jupyter_input",
|
||||||
|
|
||||||
// Related to `Deno.test()` API
|
// Related to `Deno.test()` API
|
||||||
"op_test_event_step_result_failed",
|
"op_test_event_step_result_failed",
|
||||||
|
|
Loading…
Reference in a new issue