Skip to content

Commit ebdfcee

Browse files
Write full Jupyter notebook to stdout (#7748)
## Summary When writing back notebooks via `stdout`, we need to write back the entire JSON content, not _just_ the fixed source code. Otherwise, writing the output _back_ to the file will yield an invalid notebook. Closes #7747 ## Test Plan `cargo test`
1 parent c71ff7e commit ebdfcee

File tree

4 files changed

+190
-6
lines changed

4 files changed

+190
-6
lines changed

crates/ruff_cli/src/diagnostics.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ pub(crate) fn lint_stdin(
402402
match fix_mode {
403403
flags::FixMode::Apply => {
404404
// Write the contents to stdout, regardless of whether any errors were fixed.
405-
io::stdout().write_all(transformed.source_code().as_bytes())?;
405+
transformed.write(&mut io::stdout().lock())?;
406406
}
407407
flags::FixMode::Diff => {
408408
// But only write a diff if it's non-empty.
@@ -441,7 +441,7 @@ pub(crate) fn lint_stdin(
441441

442442
// Write the contents to stdout anyway.
443443
if fix_mode.is_apply() {
444-
io::stdout().write_all(source_kind.source_code().as_bytes())?;
444+
source_kind.write(&mut io::stdout().lock())?;
445445
}
446446

447447
(result, fixed)

crates/ruff_cli/tests/integration_test.rs

+169-2
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ fn stdin_filename() {
7575
"###);
7676
}
7777

78-
#[test]
7978
/// Raise `TCH` errors in `.py` files ...
79+
#[test]
8080
fn stdin_source_type_py() {
8181
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
8282
.args(STDIN_BASE_OPTIONS)
@@ -136,7 +136,7 @@ fn stdin_json() {
136136
}
137137

138138
#[test]
139-
fn stdin_fix() {
139+
fn stdin_fix_py() {
140140
let args = ["--fix"];
141141
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
142142
.args(STDIN_BASE_OPTIONS)
@@ -153,6 +153,173 @@ fn stdin_fix() {
153153
"###);
154154
}
155155

156+
#[test]
157+
fn stdin_fix_jupyter() {
158+
let args = ["--fix", "--stdin-filename", "Jupyter.ipynb"];
159+
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
160+
.args(STDIN_BASE_OPTIONS)
161+
.args(args)
162+
.pass_stdin(r#"{
163+
"cells": [
164+
{
165+
"cell_type": "code",
166+
"execution_count": 1,
167+
"id": "dccc687c-96e2-4604-b957-a8a89b5bec06",
168+
"metadata": {},
169+
"outputs": [],
170+
"source": [
171+
"import os"
172+
]
173+
},
174+
{
175+
"cell_type": "markdown",
176+
"id": "19e1b029-f516-4662-a9b9-623b93edac1a",
177+
"metadata": {},
178+
"source": [
179+
"Foo"
180+
]
181+
},
182+
{
183+
"cell_type": "code",
184+
"execution_count": 2,
185+
"id": "cdce7b92-b0fb-4c02-86f6-e233b26fa84f",
186+
"metadata": {},
187+
"outputs": [],
188+
"source": [
189+
"import sys"
190+
]
191+
},
192+
{
193+
"cell_type": "code",
194+
"execution_count": 3,
195+
"id": "e40b33d2-7fe4-46c5-bdf0-8802f3052565",
196+
"metadata": {},
197+
"outputs": [
198+
{
199+
"name": "stdout",
200+
"output_type": "stream",
201+
"text": [
202+
"1\n"
203+
]
204+
}
205+
],
206+
"source": [
207+
"print(1)"
208+
]
209+
},
210+
{
211+
"cell_type": "code",
212+
"execution_count": null,
213+
"id": "a1899bc8-d46f-4ec0-b1d1-e1ca0f04bf60",
214+
"metadata": {},
215+
"outputs": [],
216+
"source": []
217+
}
218+
],
219+
"metadata": {
220+
"kernelspec": {
221+
"display_name": "Python 3 (ipykernel)",
222+
"language": "python",
223+
"name": "python3"
224+
},
225+
"language_info": {
226+
"codemirror_mode": {
227+
"name": "ipython",
228+
"version": 3
229+
},
230+
"file_extension": ".py",
231+
"mimetype": "text/x-python",
232+
"name": "python",
233+
"nbconvert_exporter": "python",
234+
"pygments_lexer": "ipython3",
235+
"version": "3.11.2"
236+
}
237+
},
238+
"nbformat": 4,
239+
"nbformat_minor": 5
240+
}"#), @r###"
241+
success: true
242+
exit_code: 0
243+
----- stdout -----
244+
{
245+
"cells": [
246+
{
247+
"cell_type": "code",
248+
"execution_count": 1,
249+
"id": "dccc687c-96e2-4604-b957-a8a89b5bec06",
250+
"metadata": {},
251+
"outputs": [],
252+
"source": []
253+
},
254+
{
255+
"cell_type": "markdown",
256+
"id": "19e1b029-f516-4662-a9b9-623b93edac1a",
257+
"metadata": {},
258+
"source": [
259+
"Foo"
260+
]
261+
},
262+
{
263+
"cell_type": "code",
264+
"execution_count": 2,
265+
"id": "cdce7b92-b0fb-4c02-86f6-e233b26fa84f",
266+
"metadata": {},
267+
"outputs": [],
268+
"source": []
269+
},
270+
{
271+
"cell_type": "code",
272+
"execution_count": 3,
273+
"id": "e40b33d2-7fe4-46c5-bdf0-8802f3052565",
274+
"metadata": {},
275+
"outputs": [
276+
{
277+
"name": "stdout",
278+
"output_type": "stream",
279+
"text": [
280+
"1\n"
281+
]
282+
}
283+
],
284+
"source": [
285+
"print(1)"
286+
]
287+
},
288+
{
289+
"cell_type": "code",
290+
"execution_count": null,
291+
"id": "a1899bc8-d46f-4ec0-b1d1-e1ca0f04bf60",
292+
"metadata": {},
293+
"outputs": [],
294+
"source": []
295+
}
296+
],
297+
"metadata": {
298+
"kernelspec": {
299+
"display_name": "Python 3 (ipykernel)",
300+
"language": "python",
301+
"name": "python3"
302+
},
303+
"language_info": {
304+
"codemirror_mode": {
305+
"name": "ipython",
306+
"version": 3
307+
},
308+
"file_extension": ".py",
309+
"mimetype": "text/x-python",
310+
"name": "python",
311+
"nbconvert_exporter": "python",
312+
"pygments_lexer": "ipython3",
313+
"version": "3.11.2"
314+
}
315+
},
316+
"nbformat": 4,
317+
"nbformat_minor": 5
318+
}
319+
----- stderr -----
320+
"###);
321+
}
322+
156323
#[test]
157324
fn stdin_fix_when_not_fixable_should_still_print_contents() {
158325
let args = ["--fix"];

crates/ruff_linter/src/source_kind.rs

+15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
use std::io::Write;
2+
3+
use anyhow::Result;
4+
15
use ruff_diagnostics::SourceMap;
26
use ruff_notebook::Notebook;
37

@@ -22,10 +26,21 @@ impl SourceKind {
2226
}
2327
}
2428

29+
/// Returns the Python source code for this source kind.
2530
pub fn source_code(&self) -> &str {
2631
match self {
2732
SourceKind::Python(source) => source,
2833
SourceKind::IpyNotebook(notebook) => notebook.source_code(),
2934
}
3035
}
36+
37+
/// Write the transformed source file to the given writer.
38+
///
39+
/// For Jupyter notebooks, this will write out the notebook as JSON.
40+
pub fn write(&self, writer: &mut dyn Write) -> Result<()> {
41+
match self {
42+
SourceKind::Python(source) => writer.write_all(source.as_bytes()).map_err(Into::into),
43+
SourceKind::IpyNotebook(notebook) => notebook.write(writer).map_err(Into::into),
44+
}
45+
}
3146
}

crates/ruff_notebook/src/notebook.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -414,11 +414,13 @@ impl Notebook {
414414
}
415415

416416
/// Write the notebook back to the given [`Write`] implementor.
417-
pub fn write(&self, writer: &mut dyn Write) -> anyhow::Result<()> {
417+
pub fn write(&self, writer: &mut dyn Write) -> Result<(), NotebookError> {
418418
// https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#LL1041
419419
let formatter = serde_json::ser::PrettyFormatter::with_indent(b" ");
420420
let mut serializer = serde_json::Serializer::with_formatter(writer, formatter);
421-
SortAlphabetically(&self.raw).serialize(&mut serializer)?;
421+
SortAlphabetically(&self.raw)
422+
.serialize(&mut serializer)
423+
.map_err(NotebookError::Json)?;
422424
if self.trailing_newline {
423425
writeln!(serializer.into_inner())?;
424426
}

0 commit comments

Comments
 (0)