diff --git a/crates/ruff_notebook/resources/test/fixtures/jupyter/kernelspec_language.ipynb b/crates/ruff_notebook/resources/test/fixtures/jupyter/kernelspec_language.ipynb new file mode 100644 index 0000000000000..80a5514e1c409 --- /dev/null +++ b/crates/ruff_notebook/resources/test/fixtures/jupyter/kernelspec_language.ipynb @@ -0,0 +1,48 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Kernel spec language\n", + "\n", + "This is a test notebook for validating the fallback logic of `is_python_notebook` to check `kernelspec.language` if `language_info` is absent.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "vscode": { + "languageId": "javascript" + } + }, + "outputs": [], + "source": [ + "function add(x, y) {\n", + " return x + y;\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "print(\"hello world\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/crates/ruff_notebook/src/notebook.rs b/crates/ruff_notebook/src/notebook.rs index b2be9ebe6ae50..9098227f7c2d4 100644 --- a/crates/ruff_notebook/src/notebook.rs +++ b/crates/ruff_notebook/src/notebook.rs @@ -408,13 +408,18 @@ impl Notebook { &self.raw.metadata } - /// Return `true` if the notebook is a Python notebook, `false` otherwise. + /// Check if it's a Python notebook. + /// + /// This is determined by checking the `language_info` or `kernelspec` in the notebook + /// metadata. If neither is present, it's assumed to be a Python notebook. pub fn is_python_notebook(&self) -> bool { - self.raw - .metadata - .language_info - .as_ref() - .map_or(true, |language| language.name == "python") + if let Some(language_info) = self.raw.metadata.language_info.as_ref() { + return language_info.name == "python"; + } + if let Some(kernel_spec) = self.raw.metadata.kernelspec.as_ref() { + return kernel_spec.language.as_deref() == Some("python"); + } + true } /// Write the notebook back to the given [`Write`] implementer. @@ -456,18 +461,12 @@ mod tests { Path::new("./resources/test/fixtures/jupyter").join(path) } - #[test] - fn test_python() -> Result<(), NotebookError> { - let notebook = Notebook::from_path(¬ebook_path("valid.ipynb"))?; - assert!(notebook.is_python_notebook()); - Ok(()) - } - - #[test] - fn test_r() -> Result<(), NotebookError> { - let notebook = Notebook::from_path(¬ebook_path("R.ipynb"))?; - assert!(!notebook.is_python_notebook()); - Ok(()) + #[test_case("valid.ipynb", true)] + #[test_case("R.ipynb", false)] + #[test_case("kernelspec_language.ipynb", true)] + fn is_python_notebook(filename: &str, expected: bool) { + let notebook = Notebook::from_path(¬ebook_path(filename)).unwrap(); + assert_eq!(notebook.is_python_notebook(), expected); } #[test] @@ -597,9 +596,10 @@ print("after empty cells") Ok(()) } - #[test] - fn round_trip() { - let path = notebook_path("vscode_language_id.ipynb"); + #[test_case("vscode_language_id.ipynb")] + #[test_case("kernelspec_language.ipynb")] + fn round_trip(filename: &str) { + let path = notebook_path(filename); let expected = std::fs::read_to_string(&path).unwrap(); let actual = super::round_trip(&path).unwrap(); assert_eq!(actual, expected); diff --git a/crates/ruff_notebook/src/schema.rs b/crates/ruff_notebook/src/schema.rs index a33d041055dfc..d48b7483fedf3 100644 --- a/crates/ruff_notebook/src/schema.rs +++ b/crates/ruff_notebook/src/schema.rs @@ -169,7 +169,7 @@ pub struct CellMetadata { /// preferred language. /// pub vscode: Option, - /// Catch-all for metadata that isn't required by Ruff. + /// For additional properties that isn't required by Ruff. #[serde(flatten)] pub extra: HashMap, } @@ -190,8 +190,8 @@ pub struct RawNotebookMetadata { /// The author(s) of the notebook document pub authors: Option, /// Kernel information. - pub kernelspec: Option, - /// Kernel information. + pub kernelspec: Option, + /// Language information. pub language_info: Option, /// Original notebook format (major number) before converting the notebook between versions. /// This should never be written to a file. @@ -206,6 +206,23 @@ pub struct RawNotebookMetadata { /// Kernel information. #[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Kernelspec { + /// The language name. This isn't mentioned in the spec but is populated by various tools and + /// can be used as a fallback if [`language_info`] is missing. + /// + /// This is also used by VS Code to determine the preferred language of the notebook: + /// . + /// + /// [`language_info`]: RawNotebookMetadata::language_info + pub language: Option, + /// For additional properties that isn't required by Ruff. + #[serde(flatten)] + pub extra: HashMap, +} + +/// Language information. +#[skip_serializing_none] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct LanguageInfo { /// The codemirror mode to use for code in this language. pub codemirror_mode: Option,