feat(jupyter): allow to install and maintain multiple kernels (#29249)
Some checks are pending
ci / test release macos-aarch64 (push) Blocked by required conditions
ci / bench release linux-x86_64 (push) Blocked by required conditions
ci / lint debug linux-x86_64 (push) Blocked by required conditions
ci / lint debug macos-x86_64 (push) Blocked by required conditions
ci / lint debug windows-x86_64 (push) Blocked by required conditions
ci / test debug linux-x86_64 (push) Blocked by required conditions
ci / test release linux-x86_64 (push) Blocked by required conditions
ci / test debug macos-x86_64 (push) Blocked by required conditions
ci / test release macos-x86_64 (push) Blocked by required conditions
ci / test debug windows-x86_64 (push) Blocked by required conditions
ci / test release windows-x86_64 (push) Blocked by required conditions
ci / pre-build (push) Waiting to run
ci / test debug linux-aarch64 (push) Blocked by required conditions
ci / test release linux-aarch64 (push) Blocked by required conditions
ci / test debug macos-aarch64 (push) Blocked by required conditions
ci / build wasm32 (push) Blocked by required conditions
ci / publish canary (push) Blocked by required conditions

This commit changes the `deno jupyter` subcommand:
- `deno jupyter` now accepts additional `--name` argument to 
allow installing and maintaing multiple kernelsspec - useful when 
one wants to install a stable kernel and a debug/canary kernel
- `deno jupyter --install` now accepts additional `--display` 
argument to allow customizing display name of the kernel - the 
default one is "Deno"
- `deno jupyter --install` no longer blindly installs kernelspec, 
instead it first checks if a kernelspec already exists and if so, 
returns an error suggesting to use `--force` flag
- `deno jupyter --help` no longer shows `--unstable` flag

Closes https://github.com/denoland/deno/issues/29219
Closes https://github.com/denoland/deno/issues/29220
This commit is contained in:
Bartek Iwańczuk 2025-05-13 18:56:16 +02:00 committed by GitHub
parent 9b0707baa2
commit 5ea90de199
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 223 additions and 29 deletions

View file

@ -278,8 +278,11 @@ pub struct JSONReferenceFlags {
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct JupyterFlags { pub struct JupyterFlags {
pub install: bool, pub install: bool,
pub name: Option<String>,
pub display: Option<String>,
pub kernel: bool, pub kernel: bool,
pub conn_file: Option<String>, pub conn_file: Option<String>,
pub force: bool,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -2723,14 +2726,37 @@ fn json_reference_subcommand() -> Command {
} }
fn jupyter_subcommand() -> Command { fn jupyter_subcommand() -> Command {
command("jupyter", "Deno kernel for Jupyter notebooks", UnstableArgsConfig::ResolutionAndRuntime) command("jupyter", "Deno kernel for Jupyter notebooks", UnstableArgsConfig::None)
.arg( .arg(
Arg::new("install") Arg::new("install")
.long("install") .long("install")
.help("Installs kernelspec, requires 'jupyter' command to be available.") .help("Install a kernelspec")
.conflicts_with("kernel") .conflicts_with("kernel")
.action(ArgAction::SetTrue) .action(ArgAction::SetTrue)
) )
.arg(
Arg::new("name")
.long("name")
.short('n')
.help(cstr!("Set a name for the kernel (defaults to 'deno'). <p(245)>Useful when maintaing multiple Deno kernels.</>"))
.value_parser(value_parser!(String))
.conflicts_with("kernel")
)
.arg(
Arg::new("display")
.long("display")
.short('d')
.help(cstr!("Set a display name for the kernel (defaults to 'Deno'). <p(245)>Useful when maintaing multiple Deno kernels.</>"))
.value_parser(value_parser!(String))
.requires("install")
)
.arg(
Arg::new("force")
.long("force")
.help("Force installation of a kernel, overwriting previously existing kernelspec")
.requires("install")
.action(ArgAction::SetTrue)
)
.arg( .arg(
Arg::new("kernel") Arg::new("kernel")
.long("kernel") .long("kernel")
@ -5163,11 +5189,17 @@ fn jupyter_parse(flags: &mut Flags, matches: &mut ArgMatches) {
let conn_file = matches.remove_one::<String>("conn"); let conn_file = matches.remove_one::<String>("conn");
let kernel = matches.get_flag("kernel"); let kernel = matches.get_flag("kernel");
let install = matches.get_flag("install"); let install = matches.get_flag("install");
let display = matches.remove_one::<String>("display");
let name = matches.remove_one::<String>("name");
let force = matches.get_flag("force");
flags.subcommand = DenoSubcommand::Jupyter(JupyterFlags { flags.subcommand = DenoSubcommand::Jupyter(JupyterFlags {
install, install,
kernel, kernel,
conn_file, conn_file,
name,
display,
force,
}); });
} }
@ -11340,6 +11372,9 @@ mod tests {
install: false, install: false,
kernel: false, kernel: false,
conn_file: None, conn_file: None,
name: None,
display: None,
force: false,
}), }),
..Flags::default() ..Flags::default()
} }
@ -11353,6 +11388,65 @@ mod tests {
install: true, install: true,
kernel: false, kernel: false,
conn_file: None, conn_file: None,
name: None,
display: None,
force: false,
}),
..Flags::default()
}
);
let r = flags_from_vec(svec!["deno", "jupyter", "--install", "--force"]);
assert_eq!(
r.unwrap(),
Flags {
subcommand: DenoSubcommand::Jupyter(JupyterFlags {
install: true,
kernel: false,
conn_file: None,
name: None,
display: None,
force: true,
}),
..Flags::default()
}
);
let r = flags_from_vec(svec![
"deno",
"jupyter",
"--install",
"--name",
"debugdeno",
"--display",
"Deno (debug)"
]);
assert_eq!(
r.unwrap(),
Flags {
subcommand: DenoSubcommand::Jupyter(JupyterFlags {
install: true,
kernel: false,
conn_file: None,
name: Some("debugdeno".to_string()),
display: Some("Deno (debug)".to_string()),
force: false,
}),
..Flags::default()
}
);
let r = flags_from_vec(svec!["deno", "jupyter", "-n", "debugdeno",]);
assert_eq!(
r.unwrap(),
Flags {
subcommand: DenoSubcommand::Jupyter(JupyterFlags {
install: false,
kernel: false,
conn_file: None,
name: Some("debugdeno".to_string()),
display: None,
force: false,
}), }),
..Flags::default() ..Flags::default()
} }
@ -11372,6 +11466,9 @@ mod tests {
install: false, install: false,
kernel: true, kernel: true,
conn_file: Some(String::from("path/to/conn/file")), conn_file: Some(String::from("path/to/conn/file")),
name: None,
display: None,
force: false,
}), }),
..Flags::default() ..Flags::default()
} }
@ -11389,6 +11486,12 @@ mod tests {
r.unwrap_err(); r.unwrap_err();
let r = flags_from_vec(svec!["deno", "jupyter", "--install", "--kernel",]); let r = flags_from_vec(svec!["deno", "jupyter", "--install", "--kernel",]);
r.unwrap_err(); r.unwrap_err();
let r = flags_from_vec(svec!["deno", "jupyter", "--display", "deno"]);
r.unwrap_err();
let r = flags_from_vec(svec!["deno", "jupyter", "--kernel", "--display"]);
r.unwrap_err();
let r = flags_from_vec(svec!["deno", "jupyter", "--force"]);
r.unwrap_err();
} }
#[test] #[test]
@ -11733,6 +11836,9 @@ mod tests {
install: false, install: false,
kernel: false, kernel: false,
conn_file: None, conn_file: None,
name: None,
display: None,
force: false,
}), }),
unstable_config: UnstableConfig { unstable_config: UnstableConfig {
bare_node_builtins: true, bare_node_builtins: true,

View file

@ -3,27 +3,51 @@
use std::env::current_exe; use std::env::current_exe;
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
use std::path::PathBuf;
use deno_core::anyhow::bail;
use deno_core::anyhow::Context;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::serde_json; use deno_core::serde_json;
use deno_core::serde_json::json; use deno_core::serde_json::json;
use jupyter_runtime::dirs::user_data_dir;
static TEST_ENV_VAR_NAME: &str = "DENO_TEST_JUPYTER_PATH";
const DENO_ICON_32: &[u8] = include_bytes!("./resources/deno-logo-32x32.png"); const DENO_ICON_32: &[u8] = include_bytes!("./resources/deno-logo-32x32.png");
const DENO_ICON_64: &[u8] = include_bytes!("./resources/deno-logo-64x64.png"); const DENO_ICON_64: &[u8] = include_bytes!("./resources/deno-logo-64x64.png");
const DENO_ICON_SVG: &[u8] = include_bytes!("./resources/deno-logo-svg.svg"); const DENO_ICON_SVG: &[u8] = include_bytes!("./resources/deno-logo-svg.svg");
pub fn status() -> Result<(), AnyError> { fn get_user_data_dir() -> Result<PathBuf, AnyError> {
let user_data_dir = user_data_dir()?; Ok(if let Some(env_var) = std::env::var_os(TEST_ENV_VAR_NAME) {
PathBuf::from(env_var)
} else {
jupyter_runtime::dirs::user_data_dir()?
})
}
let kernel_spec_dir_path = user_data_dir.join("kernels").join("deno"); pub fn status(maybe_name: Option<&str>) -> Result<(), AnyError> {
let user_data_dir = get_user_data_dir()?;
let kernel_name = maybe_name.unwrap_or("deno");
let kernel_spec_dir_path = user_data_dir.join("kernels").join(kernel_name);
let kernel_spec_path = kernel_spec_dir_path.join("kernel.json"); let kernel_spec_path = kernel_spec_dir_path.join("kernel.json");
if kernel_spec_path.exists() { if kernel_spec_path.exists() {
log::info!("✅ Deno kernel already installed"); log::info!(
"✅ Deno kernel already installed at {}",
kernel_spec_dir_path.display()
);
Ok(()) Ok(())
} else { } else {
log::warn!(" Deno kernel is not yet installed, run `deno jupyter --install` to set it up"); let mut install_cmd = "deno jupyter --install".to_string();
if let Some(name) = maybe_name {
install_cmd.push_str(" --name ");
install_cmd.push_str(name);
}
log::warn!(
" Deno kernel is not yet installed, run `{}` to set it up",
install_cmd
);
Ok(()) Ok(())
} }
} }
@ -39,29 +63,69 @@ fn install_icon(
Ok(()) Ok(())
} }
pub fn install() -> Result<(), AnyError> { pub fn install(
let user_data_dir = user_data_dir()?; maybe_name: Option<&str>,
let kernel_dir = user_data_dir.join("kernels").join("deno"); maybe_display_name: Option<&str>,
force: bool,
) -> Result<(), AnyError> {
let user_data_dir = get_user_data_dir()?;
std::fs::create_dir_all(&kernel_dir)?; let kernel_name = maybe_name.unwrap_or("deno");
let kernel_spec_dir_path = user_data_dir.join("kernels").join(kernel_name);
let kernel_spec_path = kernel_spec_dir_path.join("kernel.json");
let kernel_json_path = kernel_dir.join("kernel.json"); std::fs::create_dir_all(&kernel_spec_dir_path).with_context(|| {
format!(
"Failed to create kernel directory at {}",
kernel_spec_dir_path.display()
)
})?;
if kernel_spec_path.exists() && !force {
bail!(
"Deno kernel already exists at {}, run again with `--force` to overwrite it",
kernel_spec_dir_path.display()
);
}
let display_name = maybe_display_name.unwrap_or("Deno");
let current_exe_path = current_exe()
.context("Failed to get current executable path")?
.to_string_lossy()
.to_string();
// TODO(bartlomieju): add remaining fields as per // TODO(bartlomieju): add remaining fields as per
// https://jupyter-client.readthedocs.io/en/stable/kernels.html#kernel-specs // https://jupyter-client.readthedocs.io/en/stable/kernels.html#kernel-specs
// FIXME(bartlomieju): replace `current_exe` before landing?
let json_data = json!({ let json_data = json!({
"argv": [current_exe().unwrap().to_string_lossy(), "jupyter", "--kernel", "--conn", "{connection_file}"], "argv": [current_exe_path, "jupyter", "--kernel", "--conn", "{connection_file}"],
"display_name": "Deno", "display_name": display_name,
"language": "typescript", "language": "typescript",
}); });
let f = std::fs::File::create(kernel_json_path)?; let f = std::fs::File::create(&kernel_spec_path).with_context(|| {
serde_json::to_writer_pretty(f, &json_data)?; format!(
install_icon(&kernel_dir, "logo-32x32.png", DENO_ICON_32)?; "Failed to create kernelspec file at {}",
install_icon(&kernel_dir, "logo-64x64.png", DENO_ICON_64)?; kernel_spec_path.display()
install_icon(&kernel_dir, "logo-svg.svg", DENO_ICON_SVG)?; )
})?;
serde_json::to_writer_pretty(f, &json_data).with_context(|| {
format!(
"Failed to write kernelspec file at {}",
kernel_spec_path.display()
)
})?;
let failed_icon_fn =
|| format!("Failed to copy icon to {}", kernel_spec_dir_path.display());
install_icon(&kernel_spec_dir_path, "logo-32x32.png", DENO_ICON_32)
.with_context(failed_icon_fn)?;
install_icon(&kernel_spec_dir_path, "logo-64x64.png", DENO_ICON_64)
.with_context(failed_icon_fn)?;
install_icon(&kernel_spec_dir_path, "logo-svg.svg", DENO_ICON_SVG)
.with_context(failed_icon_fn)?;
log::info!("✅ Deno kernelspec installed successfully."); log::info!(
"✅ Deno kernelspec installed successfully at {}.",
kernel_spec_dir_path.display()
);
Ok(()) Ok(())
} }

View file

@ -49,12 +49,16 @@ pub async fn kernel(
); );
if !jupyter_flags.install && !jupyter_flags.kernel { if !jupyter_flags.install && !jupyter_flags.kernel {
install::status()?; install::status(jupyter_flags.name.as_deref())?;
return Ok(()); return Ok(());
} }
if jupyter_flags.install { if jupyter_flags.install {
install::install()?; install::install(
jupyter_flags.name.as_deref(),
jupyter_flags.display.as_deref(),
jupyter_flags.force,
)?;
return Ok(()); return Ok(());
} }

View file

@ -1,8 +1,24 @@
{ {
"args": "jupyter --install", "tempDir": true,
"output": "install_command.out",
"envs": { "envs": {
"PATH": "" "PATH": "",
"DENO_TEST_JUPYTER_PATH": "$PWD"
}, },
"exitCode": 0 "steps": [{
"args": "jupyter --install",
"output": "install_command.out",
"exitCode": 0
}, {
"args": "jupyter --install",
"output": "already_installed.out",
"exitCode": 1
}, {
"args": "jupyter --install --name devdeno",
"output": "install_name.out",
"exitCode": 0
}, {
"args": "jupyter --install --force",
"output": "install_command.out",
"exitCode": 0
}]
} }

View file

@ -0,0 +1,2 @@
Warning "deno jupyter" is unstable and might change in the future.
error: Deno kernel already exists at [WILDLINE]deno, run again with `--force` to overwrite it

View file

@ -1,2 +1,2 @@
Warning "deno jupyter" is unstable and might change in the future. Warning "deno jupyter" is unstable and might change in the future.
✅ Deno kernelspec installed successfully. ✅ Deno kernelspec installed successfully at [WILDLINE]deno.

View file

@ -0,0 +1,2 @@
Warning "deno jupyter" is unstable and might change in the future.
✅ Deno kernelspec installed successfully at [WILDLINE]devdeno.