Skip to content

Commit 73160dc

Browse files
dhruvmanilaAlexWaygood
authored andcommitted
Stabilize support for Jupyter Notebooks (#12878)
Co-authored-by: Alex Waygood <[email protected]> Closes: #12456 Closes: astral-sh/ruff-vscode#546
1 parent 15aa5a6 commit 73160dc

File tree

13 files changed

+68
-160
lines changed

13 files changed

+68
-160
lines changed

crates/ruff/src/commands/check.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,7 @@ mod test {
268268

269269
// Run
270270
let diagnostics = check(
271-
// Notebooks are not included by default
272-
&[tempdir.path().to_path_buf(), notebook],
271+
&[tempdir.path().to_path_buf()],
273272
&pyproject_config,
274273
&ConfigArguments::default(),
275274
flags::Cache::Disabled,

crates/ruff/tests/lint.rs

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1806,7 +1806,7 @@ select = ["UP006"]
18061806
}
18071807

18081808
#[test]
1809-
fn checks_notebooks_in_preview_mode() -> anyhow::Result<()> {
1809+
fn checks_notebooks_in_stable() -> anyhow::Result<()> {
18101810
let tempdir = TempDir::new()?;
18111811
std::fs::write(
18121812
tempdir.path().join("main.ipynb"),
@@ -1853,7 +1853,6 @@ fn checks_notebooks_in_preview_mode() -> anyhow::Result<()> {
18531853
.args(STDIN_BASE_OPTIONS)
18541854
.arg("--select")
18551855
.arg("F401")
1856-
.arg("--preview")
18571856
.current_dir(&tempdir)
18581857
, @r###"
18591858
success: false
@@ -1867,64 +1866,3 @@ fn checks_notebooks_in_preview_mode() -> anyhow::Result<()> {
18671866
"###);
18681867
Ok(())
18691868
}
1870-
1871-
#[test]
1872-
fn ignores_notebooks_in_stable() -> anyhow::Result<()> {
1873-
let tempdir = TempDir::new()?;
1874-
std::fs::write(
1875-
tempdir.path().join("main.ipynb"),
1876-
r#"
1877-
{
1878-
"cells": [
1879-
{
1880-
"cell_type": "code",
1881-
"execution_count": null,
1882-
"id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d",
1883-
"metadata": {},
1884-
"outputs": [],
1885-
"source": [
1886-
"import random"
1887-
]
1888-
}
1889-
],
1890-
"metadata": {
1891-
"kernelspec": {
1892-
"display_name": "Python 3 (ipykernel)",
1893-
"language": "python",
1894-
"name": "python3"
1895-
},
1896-
"language_info": {
1897-
"codemirror_mode": {
1898-
"name": "ipython",
1899-
"version": 3
1900-
},
1901-
"file_extension": ".py",
1902-
"mimetype": "text/x-python",
1903-
"name": "python",
1904-
"nbconvert_exporter": "python",
1905-
"pygments_lexer": "ipython3",
1906-
"version": "3.12.0"
1907-
}
1908-
},
1909-
"nbformat": 4,
1910-
"nbformat_minor": 5
1911-
}
1912-
"#,
1913-
)?;
1914-
1915-
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
1916-
.args(STDIN_BASE_OPTIONS)
1917-
.arg("--select")
1918-
.arg("F401")
1919-
.current_dir(&tempdir)
1920-
, @r###"
1921-
success: true
1922-
exit_code: 0
1923-
----- stdout -----
1924-
All checks passed!
1925-
1926-
----- stderr -----
1927-
warning: No Python files found under the given path(s)
1928-
"###);
1929-
Ok(())
1930-
}

crates/ruff/tests/snapshots/show_settings__display_default_settings.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ file_resolver.force_exclude = false
6060
file_resolver.include = [
6161
"*.py",
6262
"*.pyi",
63+
"*.ipynb",
6364
"**/pyproject.toml",
6465
]
6566
file_resolver.extend_include = []

crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_comparison.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ use super::super::helpers::at_last_top_level_expression_in_cell;
2525
/// assert foo == bar, "`foo` and `bar` should be equal."
2626
/// ```
2727
///
28+
/// ## Notebook behavior
29+
/// For Jupyter Notebooks, this rule is not applied to the last top-level expression in a cell.
30+
/// This is because it's common to have a notebook cell that ends with an expression,
31+
/// which will result in the `repr` of the evaluated expression being printed as the cell's output.
32+
///
2833
/// ## References
2934
/// - [Python documentation: `assert` statement](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement)
3035
#[violation]
@@ -43,9 +48,6 @@ impl Violation for UselessComparison {
4348
/// B015
4449
pub(crate) fn useless_comparison(checker: &mut Checker, expr: &Expr) {
4550
if expr.is_compare_expr() {
46-
// For Jupyter Notebooks, ignore the last top-level expression for each cell.
47-
// This is because it's common to have a cell that ends with an expression
48-
// to display it's value.
4951
if checker.source_type.is_ipynb()
5052
&& at_last_top_level_expression_in_cell(
5153
checker.semantic(),

crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_expression.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ use super::super::helpers::at_last_top_level_expression_in_cell;
2626
/// foo = 1 + 1
2727
/// ```
2828
///
29+
/// ## Notebook behavior
30+
/// For Jupyter Notebooks, this rule is not applied to the last top-level expression in a cell.
31+
/// This is because it's common to have a notebook cell that ends with an expression,
32+
/// which will result in the `repr` of the evaluated expression being printed as the cell's output.
33+
///
2934
/// ## Known problems
3035
/// This rule ignores expression types that are commonly used for their side
3136
/// effects, such as function calls.
@@ -81,9 +86,6 @@ pub(crate) fn useless_expression(checker: &mut Checker, value: &Expr) {
8186
return;
8287
}
8388

84-
// For Jupyter Notebooks, ignore the last top-level expression for each cell.
85-
// This is because it's common to have a cell that ends with an expression
86-
// to display it's value.
8789
if checker.source_type.is_ipynb()
8890
&& at_last_top_level_expression_in_cell(
8991
checker.semantic(),

crates/ruff_linter/src/rules/pycodestyle/rules/module_import_not_at_top_of_file.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ use ruff_text_size::Ranged;
66
use crate::checkers::ast::Checker;
77

88
/// ## What it does
9-
/// Checks for imports that are not at the top of the file. For Jupyter notebooks, this
10-
/// checks for imports that are not at the top of the cell.
9+
/// Checks for imports that are not at the top of the file.
1110
///
1211
/// ## Why is this bad?
1312
/// According to [PEP 8], "imports are always put at the top of the file, just after any
@@ -36,6 +35,9 @@ use crate::checkers::ast::Checker;
3635
/// a = 1
3736
/// ```
3837
///
38+
/// ## Notebook behavior
39+
/// For Jupyter notebooks, this rule checks for imports that are not at the top of a *cell*.
40+
///
3941
/// [PEP 8]: https://peps.python.org/pep-0008/#imports
4042
#[violation]
4143
pub struct ModuleImportNotAtTopOfFile {

crates/ruff_linter/src/rules/pydocstyle/rules/not_missing.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ use crate::registry::Rule;
5353
/// def calculate_speed(distance: float, time: float) -> float: ...
5454
/// ```
5555
///
56+
/// ## Notebook behavior
57+
/// This rule is ignored for Jupyter Notebooks.
58+
///
5659
/// ## References
5760
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
5861
/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/)

crates/ruff_workspace/src/configuration.rs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -230,15 +230,9 @@ impl Configuration {
230230
extend_exclude: FilePatternSet::try_from_iter(self.extend_exclude)?,
231231
extend_include: FilePatternSet::try_from_iter(self.extend_include)?,
232232
force_exclude: self.force_exclude.unwrap_or(false),
233-
include: FilePatternSet::try_from_iter(self.include.unwrap_or_else(|| {
234-
let mut include = INCLUDE.to_vec();
235-
236-
if global_preview.is_enabled() {
237-
include.push(FilePattern::Builtin("*.ipynb"));
238-
}
239-
240-
include
241-
}))?,
233+
include: FilePatternSet::try_from_iter(
234+
self.include.unwrap_or_else(|| INCLUDE.to_vec()),
235+
)?,
242236
respect_gitignore: self.respect_gitignore.unwrap_or(true),
243237
project_root: project_root.to_path_buf(),
244238
},

crates/ruff_workspace/src/options.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,13 +241,11 @@ pub struct Options {
241241
/// included here not for configuration but because we lint whether e.g. the
242242
/// `[project]` matches the schema.
243243
///
244-
/// If [preview](https://docs.astral.sh/ruff/preview/) is enabled, the default
245-
/// includes notebook files (`.ipynb` extension). You can exclude them by adding
246-
/// `*.ipynb` to [`extend-exclude`](#extend-exclude).
244+
/// Notebook files (`.ipynb` extension) are included by default on Ruff 0.6.0+.
247245
///
248246
/// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
249247
#[option(
250-
default = r#"["*.py", "*.pyi", "**/pyproject.toml"]"#,
248+
default = r#"["*.py", "*.pyi", "*.ipynb", "**/pyproject.toml"]"#,
251249
value_type = "list[str]",
252250
example = r#"
253251
include = ["*.py"]

crates/ruff_workspace/src/settings.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ pub(crate) static EXCLUDE: &[FilePattern] = &[
137137
pub(crate) static INCLUDE: &[FilePattern] = &[
138138
FilePattern::Builtin("*.py"),
139139
FilePattern::Builtin("*.pyi"),
140+
FilePattern::Builtin("*.ipynb"),
140141
FilePattern::Builtin("**/pyproject.toml"),
141142
];
142143

docs/configuration.md

Lines changed: 39 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -339,23 +339,9 @@ For example, `ruff check /path/to/excluded/file.py` will always lint `file.py`.
339339

340340
### Default inclusions
341341

342-
By default, Ruff will discover files matching `*.py`, `*.ipy`, or `pyproject.toml`.
342+
By default, Ruff will discover files matching `*.py`, `*.pyi`, `*.ipynb`, or `pyproject.toml`.
343343

344344
To lint or format files with additional file extensions, use the [`extend-include`](settings.md#extend-include) setting.
345-
346-
=== "pyproject.toml"
347-
348-
```toml
349-
[tool.ruff]
350-
extend-include = ["*.ipynb"]
351-
```
352-
353-
=== "ruff.toml"
354-
355-
```toml
356-
extend-include = ["*.ipynb"]
357-
```
358-
359345
You can also change the default selection using the [`include`](settings.md#include) setting.
360346

361347

@@ -378,78 +364,82 @@ You can also change the default selection using the [`include`](settings.md#incl
378364

379365
## Jupyter Notebook discovery
380366

381-
Ruff has built-in support for [Jupyter Notebooks](https://jupyter.org/).
382-
383-
!!! info
384-
Notebooks are linted and formatted by default when using [preview mode](preview.md).
385-
You can opt-out of notebook linting and formatting by adding `*.ipynb` to [`extend-exclude`](settings.md#extend-exclude).
367+
Ruff has built-in support for linting and formatting [Jupyter Notebooks](https://jupyter.org/),
368+
which are linted and formatted by default on version `0.6.0` and higher.
386369

387-
To opt in to linting and formatting Jupyter Notebook (`.ipynb`) files, add the `*.ipynb` pattern to
388-
your [`extend-include`](settings.md#extend-include) setting, like so:
370+
If you'd prefer to either only lint or only format Jupyter Notebook files, you can use the
371+
section-specific `exclude` option to do so. For example, the following would only lint Jupyter
372+
Notebook files and not format them:
389373

390374
=== "pyproject.toml"
391375

392376
```toml
393-
[tool.ruff]
394-
extend-include = ["*.ipynb"]
377+
[tool.ruff.format]
378+
exclude = ["*.ipynb"]
395379
```
396380

397381
=== "ruff.toml"
398382

399383
```toml
400-
extend-include = ["*.ipynb"]
384+
[format]
385+
exclude = ["*.ipynb"]
401386
```
402387

403-
This will prompt Ruff to discover Jupyter Notebook (`.ipynb`) files in any specified
404-
directories, then lint and format them accordingly.
405-
406-
If you'd prefer to either only lint or only format Jupyter Notebook files, you can use the
407-
section specific `exclude` option to do so. For example, the following would only lint Jupyter
408-
Notebook files and not format them:
388+
And, conversely, the following would only format Jupyter Notebook files and not lint them:
409389

410390
=== "pyproject.toml"
411391

412392
```toml
413-
[tool.ruff]
414-
extend-include = ["*.ipynb"]
415-
416-
[tool.ruff.format]
393+
[tool.ruff.lint]
417394
exclude = ["*.ipynb"]
418395
```
419396

420397
=== "ruff.toml"
421398

422399
```toml
423-
extend-include = ["*.ipynb"]
424-
425-
[format]
400+
[lint]
426401
exclude = ["*.ipynb"]
427402
```
428403

429-
And, conversely, the following would only format Jupyter Notebook files and not lint them:
404+
You can completely disable Jupyter Notebook support by updating the
405+
[`extend-exclude`](settings.md#extend-exclude) setting:
430406

431407
=== "pyproject.toml"
432408

433409
```toml
434410
[tool.ruff]
435-
extend-include = ["*.ipynb"]
436-
437-
[tool.ruff.lint]
438-
exclude = ["*.ipynb"]
411+
extend-exclude = ["*.ipynb"]
439412
```
440413

441414
=== "ruff.toml"
442415

443416
```toml
444-
extend-include = ["*.ipynb"]
417+
extend-exclude = ["*.ipynb"]
418+
```
445419

446-
[lint]
447-
exclude = ["*.ipynb"]
420+
If you'd like to ignore certain rules specifically for Jupyter Notebook files, you can do so by
421+
using the [`per-file-ignores`](settings.md#per-file-ignores) setting:
422+
423+
=== "pyproject.toml"
424+
425+
```toml
426+
[tool.ruff.lint.per-file-ignores]
427+
"*.ipynb" = ["T20"]
428+
```
429+
430+
=== "ruff.toml"
431+
432+
```toml
433+
[lint.per-file-ignores]
434+
"*.ipynb" = ["T20"]
448435
```
449436

450-
Alternatively, pass the notebook file(s) to `ruff` on the command-line directly. For example,
451-
`ruff check /path/to/notebook.ipynb` will always lint `notebook.ipynb`. Similarly,
452-
`ruff format /path/to/notebook.ipynb` will always format `notebook.ipynb`.
437+
Some rules have different behavior when applied to Jupyter Notebook files. For
438+
example, when applied to `.py` files the
439+
[`module-import-not-at-top-of-file` (`E402`)](rules/module-import-not-at-top-of-file.md)
440+
rule detect imports at the top of a file, but for notebooks it detects imports at the top of a
441+
**cell**. For a given rule, the rule's documentation will always specify if it has different
442+
behavior when applied to Jupyter Notebook files.
453443

454444
## Command-line interface
455445

docs/faq.md

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -398,30 +398,8 @@ them. You can find the supported settings in the [API reference](settings.md#lin
398398

399399
## Does Ruff support Jupyter Notebooks?
400400

401-
Ruff has built-in support for linting [Jupyter Notebooks](https://jupyter.org/).
402-
403-
To opt in to linting Jupyter Notebook (`.ipynb`) files, add the `*.ipynb` pattern to your
404-
[`extend-include`](settings.md#extend-include) setting, like so:
405-
406-
=== "pyproject.toml"
407-
408-
```toml
409-
[tool.ruff]
410-
extend-include = ["*.ipynb"]
411-
```
412-
413-
=== "ruff.toml"
414-
415-
```toml
416-
extend-include = ["*.ipynb"]
417-
```
418-
419-
This will prompt Ruff to discover Jupyter Notebook (`.ipynb`) files in any specified
420-
directories, then lint and format them accordingly.
421-
422-
Alternatively, pass the notebook file(s) to `ruff` on the command-line directly. For example,
423-
`ruff check /path/to/notebook.ipynb` will always lint `notebook.ipynb`. Similarly,
424-
`ruff format /path/to/notebook.ipynb` will always format `notebook.ipynb`.
401+
Ruff has built-in support for linting and formatting [Jupyter Notebooks](https://jupyter.org/). Refer to the
402+
[Jupyter Notebook section](configuration.md#jupyter-notebook-discovery) for more details.
425403

426404
Ruff also integrates with [nbQA](https://github.com/nbQA-dev/nbQA), a tool for running linters and
427405
code formatters over Jupyter Notebooks.

0 commit comments

Comments
 (0)