Skip to content

Commit afcd00d

Browse files
Create ruff_notebook crate (#7039)
## Summary This PR moves `ruff/jupyter` into its own `ruff_notebook` crate. Beyond the move itself, there were a few challenges: 1. `ruff_notebook` relies on the source map abstraction. I've moved the source map into `ruff_diagnostics`, since it doesn't have any dependencies on its own and is used alongside diagnostics. 2. `ruff_notebook` has a couple tests for end-to-end linting and autofixing. I had to leave these tests in `ruff` itself. 3. We had code in `ruff/jupyter` that relied on Python lexing, in order to provide a more targeted error message in the event that a user saves a `.py` file with a `.ipynb` extension. I removed this in order to avoid a dependency on the parser, it felt like it wasn't worth retaining just for that dependency. ## Test Plan `cargo test`
1 parent 08e2467 commit afcd00d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+274
-253
lines changed

CONTRIBUTING.md

+1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ At time of writing, the repository includes the following crates:
129129
intermediate representation. The backend for `ruff_python_formatter`.
130130
- `crates/ruff_index`: library crate inspired by `rustc_index`.
131131
- `crates/ruff_macros`: proc macro crate containing macros used by Ruff.
132+
- `crates/ruff_notebook`: library crate for parsing and manipulating Jupyter notebooks.
132133
- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities.
133134
- `crates/ruff_python_codegen`: library crate containing utilities for generating Python source code.
134135
- `crates/ruff_python_formatter`: library crate implementing the Python formatter. Emits an

Cargo.lock

+22-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ruff/Cargo.toml

+2-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ name = "ruff"
1818
ruff_cache = { path = "../ruff_cache" }
1919
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
2020
ruff_index = { path = "../ruff_index" }
21+
ruff_notebook = { path = "../ruff_notebook" }
2122
ruff_macros = { path = "../ruff_macros" }
2223
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
2324
ruff_python_codegen = { path = "../ruff_python_codegen" }
@@ -64,17 +65,15 @@ schemars = { workspace = true, optional = true }
6465
semver = { version = "1.0.16" }
6566
serde = { workspace = true }
6667
serde_json = { workspace = true }
67-
serde_with = { version = "3.0.0" }
6868
similar = { workspace = true }
6969
smallvec = { workspace = true }
7070
strum = { workspace = true }
7171
strum_macros = { workspace = true }
72-
thiserror = { version = "1.0.43" }
72+
thiserror = { workspace = true }
7373
toml = { workspace = true }
7474
typed-arena = { version = "2.0.2" }
7575
unicode-width = { workspace = true }
7676
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
77-
uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
7877
wsl = { version = "0.1.0" }
7978

8079
[dev-dependencies]

crates/ruff/src/autofix/mod.rs

+14-53
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,15 @@ use std::collections::BTreeSet;
44
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
55
use rustc_hash::{FxHashMap, FxHashSet};
66

7-
use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel};
7+
use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel, SourceMap};
88
use ruff_source_file::Locator;
99

10-
use crate::autofix::source_map::SourceMap;
1110
use crate::linter::FixTable;
1211
use crate::registry::{AsRule, Rule};
1312

1413
pub(crate) mod codemods;
1514
pub(crate) mod edits;
1615
pub(crate) mod snippet;
17-
pub(crate) mod source_map;
1816

1917
pub(crate) struct FixResult {
2018
/// The resulting source code, after applying all fixes.
@@ -140,10 +138,9 @@ fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Orderi
140138
mod tests {
141139
use ruff_text_size::{Ranged, TextSize};
142140

143-
use ruff_diagnostics::{Diagnostic, Edit, Fix};
141+
use ruff_diagnostics::{Diagnostic, Edit, Fix, SourceMarker};
144142
use ruff_source_file::Locator;
145143

146-
use crate::autofix::source_map::SourceMarker;
147144
use crate::autofix::{apply_fixes, FixResult};
148145
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
149146

@@ -207,14 +204,8 @@ print("hello world")
207204
assert_eq!(
208205
source_map.markers(),
209206
&[
210-
SourceMarker {
211-
source: 10.into(),
212-
dest: 10.into(),
213-
},
214-
SourceMarker {
215-
source: 10.into(),
216-
dest: 21.into(),
217-
},
207+
SourceMarker::new(10.into(), 10.into(),),
208+
SourceMarker::new(10.into(), 21.into(),),
218209
]
219210
);
220211
}
@@ -250,14 +241,8 @@ class A(Bar):
250241
assert_eq!(
251242
source_map.markers(),
252243
&[
253-
SourceMarker {
254-
source: 8.into(),
255-
dest: 8.into(),
256-
},
257-
SourceMarker {
258-
source: 14.into(),
259-
dest: 11.into(),
260-
},
244+
SourceMarker::new(8.into(), 8.into(),),
245+
SourceMarker::new(14.into(), 11.into(),),
261246
]
262247
);
263248
}
@@ -289,14 +274,8 @@ class A:
289274
assert_eq!(
290275
source_map.markers(),
291276
&[
292-
SourceMarker {
293-
source: 7.into(),
294-
dest: 7.into()
295-
},
296-
SourceMarker {
297-
source: 15.into(),
298-
dest: 7.into()
299-
}
277+
SourceMarker::new(7.into(), 7.into()),
278+
SourceMarker::new(15.into(), 7.into()),
300279
]
301280
);
302281
}
@@ -332,22 +311,10 @@ class A(object):
332311
assert_eq!(
333312
source_map.markers(),
334313
&[
335-
SourceMarker {
336-
source: 8.into(),
337-
dest: 8.into()
338-
},
339-
SourceMarker {
340-
source: 16.into(),
341-
dest: 8.into()
342-
},
343-
SourceMarker {
344-
source: 22.into(),
345-
dest: 14.into(),
346-
},
347-
SourceMarker {
348-
source: 30.into(),
349-
dest: 14.into(),
350-
}
314+
SourceMarker::new(8.into(), 8.into()),
315+
SourceMarker::new(16.into(), 8.into()),
316+
SourceMarker::new(22.into(), 14.into(),),
317+
SourceMarker::new(30.into(), 14.into(),),
351318
]
352319
);
353320
}
@@ -382,14 +349,8 @@ class A:
382349
assert_eq!(
383350
source_map.markers(),
384351
&[
385-
SourceMarker {
386-
source: 7.into(),
387-
dest: 7.into(),
388-
},
389-
SourceMarker {
390-
source: 15.into(),
391-
dest: 7.into(),
392-
}
352+
SourceMarker::new(7.into(), 7.into(),),
353+
SourceMarker::new(15.into(), 7.into(),),
393354
]
394355
);
395356
}

crates/ruff/src/lib.rs

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ mod doc_lines;
2020
mod docstrings;
2121
pub mod fs;
2222
mod importer;
23-
pub mod jupyter;
2423
mod lex;
2524
pub mod line_width;
2625
pub mod linter;

crates/ruff/src/linter.rs

+132-3
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@ use anyhow::{anyhow, Result};
66
use colored::Colorize;
77
use itertools::Itertools;
88
use log::error;
9-
use ruff_python_parser::lexer::LexResult;
10-
use ruff_python_parser::{AsMode, ParseError};
119
use rustc_hash::FxHashMap;
1210

1311
use ruff_diagnostics::Diagnostic;
1412
use ruff_python_ast::imports::ImportMap;
1513
use ruff_python_ast::PySourceType;
1614
use ruff_python_codegen::Stylist;
1715
use ruff_python_index::Indexer;
18-
16+
use ruff_python_parser::lexer::LexResult;
17+
use ruff_python_parser::{AsMode, ParseError};
1918
use ruff_source_file::{Locator, SourceFileBuilder};
2019
use ruff_text_size::Ranged;
2120

@@ -609,3 +608,133 @@ This indicates a bug in `{}`. If you could open an issue at:
609608
);
610609
}
611610
}
611+
612+
#[cfg(test)]
613+
mod tests {
614+
use std::path::Path;
615+
616+
use anyhow::Result;
617+
use test_case::test_case;
618+
619+
use ruff_notebook::{Notebook, NotebookError};
620+
621+
use crate::registry::Rule;
622+
use crate::source_kind::SourceKind;
623+
use crate::test::{test_contents, test_notebook_path, TestedNotebook};
624+
use crate::{assert_messages, settings};
625+
626+
/// Construct a path to a Jupyter notebook in the `resources/test/fixtures/jupyter` directory.
627+
fn notebook_path(path: impl AsRef<Path>) -> std::path::PathBuf {
628+
Path::new("../ruff_notebook/resources/test/fixtures/jupyter").join(path)
629+
}
630+
631+
#[test]
632+
fn test_import_sorting() -> Result<(), NotebookError> {
633+
let actual = notebook_path("isort.ipynb");
634+
let expected = notebook_path("isort_expected.ipynb");
635+
let TestedNotebook {
636+
messages,
637+
source_notebook,
638+
..
639+
} = test_notebook_path(
640+
&actual,
641+
expected,
642+
&settings::Settings::for_rule(Rule::UnsortedImports),
643+
)?;
644+
assert_messages!(messages, actual, source_notebook);
645+
Ok(())
646+
}
647+
648+
#[test]
649+
fn test_ipy_escape_command() -> Result<(), NotebookError> {
650+
let actual = notebook_path("ipy_escape_command.ipynb");
651+
let expected = notebook_path("ipy_escape_command_expected.ipynb");
652+
let TestedNotebook {
653+
messages,
654+
source_notebook,
655+
..
656+
} = test_notebook_path(
657+
&actual,
658+
expected,
659+
&settings::Settings::for_rule(Rule::UnusedImport),
660+
)?;
661+
assert_messages!(messages, actual, source_notebook);
662+
Ok(())
663+
}
664+
665+
#[test]
666+
fn test_unused_variable() -> Result<(), NotebookError> {
667+
let actual = notebook_path("unused_variable.ipynb");
668+
let expected = notebook_path("unused_variable_expected.ipynb");
669+
let TestedNotebook {
670+
messages,
671+
source_notebook,
672+
..
673+
} = test_notebook_path(
674+
&actual,
675+
expected,
676+
&settings::Settings::for_rule(Rule::UnusedVariable),
677+
)?;
678+
assert_messages!(messages, actual, source_notebook);
679+
Ok(())
680+
}
681+
682+
#[test]
683+
fn test_json_consistency() -> Result<()> {
684+
let actual_path = notebook_path("before_fix.ipynb");
685+
let expected_path = notebook_path("after_fix.ipynb");
686+
687+
let TestedNotebook {
688+
linted_notebook: fixed_notebook,
689+
..
690+
} = test_notebook_path(
691+
actual_path,
692+
&expected_path,
693+
&settings::Settings::for_rule(Rule::UnusedImport),
694+
)?;
695+
let mut writer = Vec::new();
696+
fixed_notebook.write(&mut writer)?;
697+
let actual = String::from_utf8(writer)?;
698+
let expected = std::fs::read_to_string(expected_path)?;
699+
assert_eq!(actual, expected);
700+
Ok(())
701+
}
702+
703+
#[test_case(Path::new("before_fix.ipynb"), true; "trailing_newline")]
704+
#[test_case(Path::new("no_trailing_newline.ipynb"), false; "no_trailing_newline")]
705+
fn test_trailing_newline(path: &Path, trailing_newline: bool) -> Result<()> {
706+
let notebook = Notebook::from_path(&notebook_path(path))?;
707+
assert_eq!(notebook.trailing_newline(), trailing_newline);
708+
709+
let mut writer = Vec::new();
710+
notebook.write(&mut writer)?;
711+
let string = String::from_utf8(writer)?;
712+
assert_eq!(string.ends_with('\n'), trailing_newline);
713+
714+
Ok(())
715+
}
716+
717+
// Version <4.5, don't emit cell ids
718+
#[test_case(Path::new("no_cell_id.ipynb"), false; "no_cell_id")]
719+
// Version 4.5, cell ids are missing and need to be added
720+
#[test_case(Path::new("add_missing_cell_id.ipynb"), true; "add_missing_cell_id")]
721+
fn test_cell_id(path: &Path, has_id: bool) -> Result<()> {
722+
let source_notebook = Notebook::from_path(&notebook_path(path))?;
723+
let source_kind = SourceKind::IpyNotebook(source_notebook);
724+
let (_, transformed) = test_contents(
725+
&source_kind,
726+
path,
727+
&settings::Settings::for_rule(Rule::UnusedImport),
728+
);
729+
let linted_notebook = transformed.into_owned().expect_ipy_notebook();
730+
let mut writer = Vec::new();
731+
linted_notebook.write(&mut writer)?;
732+
let actual = String::from_utf8(writer)?;
733+
if has_id {
734+
assert!(actual.contains(r#""id": ""#));
735+
} else {
736+
assert!(!actual.contains(r#""id":"#));
737+
}
738+
Ok(())
739+
}
740+
}

crates/ruff/src/logging.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ use ruff_python_parser::{ParseError, ParseErrorType};
1212
use ruff_source_file::{OneIndexed, SourceCode, SourceLocation};
1313

1414
use crate::fs;
15-
use crate::jupyter::Notebook;
1615
use crate::source_kind::SourceKind;
16+
use ruff_notebook::Notebook;
1717

1818
pub static WARNINGS: Lazy<Mutex<Vec<&'static str>>> = Lazy::new(Mutex::default);
1919

0 commit comments

Comments
 (0)