Compare commits
15 Commits
charlie/cl
...
v0.0.287
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f3a950f6f | ||
|
|
dea65536e9 | ||
|
|
fbc9b5a604 | ||
|
|
253a241f5d | ||
|
|
33806b8b7c | ||
|
|
afcd00da56 | ||
|
|
08e246764f | ||
|
|
0489bbc54c | ||
|
|
60132da7bb | ||
|
|
17a44c0078 | ||
|
|
376d3caf47 | ||
|
|
51d69b448c | ||
|
|
f7dca3d958 | ||
|
|
7c1aa98f43 | ||
|
|
68f605e80a |
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
@@ -4,8 +4,10 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "12:00"
|
||||
timezone: "America/New_York"
|
||||
commit-message:
|
||||
prefix: "ci(deps)"
|
||||
labels: ["internal"]
|
||||
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels: ["internal"]
|
||||
|
||||
2
.github/workflows/docs.yaml
vendored
2
.github/workflows/docs.yaml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
run: mkdocs build --strict -f mkdocs.generated.yml
|
||||
- name: "Deploy to Cloudflare Pages"
|
||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||
uses: cloudflare/wrangler-action@v3.1.0
|
||||
uses: cloudflare/wrangler-action@v3.1.1
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
|
||||
2
.github/workflows/playground.yaml
vendored
2
.github/workflows/playground.yaml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
working-directory: playground
|
||||
- name: "Deploy to Cloudflare Pages"
|
||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||
uses: cloudflare/wrangler-action@v3.1.0
|
||||
uses: cloudflare/wrangler-action@v3.1.1
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
|
||||
@@ -129,6 +129,7 @@ At time of writing, the repository includes the following crates:
|
||||
intermediate representation. The backend for `ruff_python_formatter`.
|
||||
- `crates/ruff_index`: library crate inspired by `rustc_index`.
|
||||
- `crates/ruff_macros`: proc macro crate containing macros used by Ruff.
|
||||
- `crates/ruff_notebook`: library crate for parsing and manipulating Jupyter notebooks.
|
||||
- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities.
|
||||
- `crates/ruff_python_codegen`: library crate containing utilities for generating Python source code.
|
||||
- `crates/ruff_python_formatter`: library crate implementing the Python formatter. Emits an
|
||||
|
||||
548
Cargo.lock
generated
548
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.0.286
|
||||
rev: v0.0.287
|
||||
hooks:
|
||||
- id: ruff
|
||||
```
|
||||
@@ -398,6 +398,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- [Pydantic](https://github.com/pydantic/pydantic)
|
||||
- [Pylint](https://github.com/PyCQA/pylint)
|
||||
- [Reflex](https://github.com/reflex-dev/reflex)
|
||||
- [Rippling](https://rippling.com)
|
||||
- [Robyn](https://github.com/sansyrox/robyn)
|
||||
- Scale AI ([Launch SDK](https://github.com/scaleapi/launch-python-client))
|
||||
- Snowflake ([SnowCLI](https://github.com/Snowflake-Labs/snowcli))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.286"
|
||||
version = "0.0.287"
|
||||
description = """
|
||||
Convert Flake8 configuration files to Ruff configuration files.
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.286"
|
||||
version = "0.0.287"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -18,6 +18,7 @@ name = "ruff"
|
||||
ruff_cache = { path = "../ruff_cache" }
|
||||
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
|
||||
ruff_index = { path = "../ruff_index" }
|
||||
ruff_notebook = { path = "../ruff_notebook" }
|
||||
ruff_macros = { path = "../ruff_macros" }
|
||||
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
|
||||
ruff_python_codegen = { path = "../ruff_python_codegen" }
|
||||
@@ -64,17 +65,15 @@ schemars = { workspace = true, optional = true }
|
||||
semver = { version = "1.0.16" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { version = "3.0.0" }
|
||||
similar = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
thiserror = { version = "1.0.43" }
|
||||
thiserror = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
typed-arena = { version = "2.0.2" }
|
||||
unicode-width = { workspace = true }
|
||||
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
|
||||
uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
|
||||
wsl = { version = "0.1.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -3,16 +3,20 @@ def bar():
|
||||
|
||||
|
||||
def foo():
|
||||
"""foo""" # OK
|
||||
"""foo""" # OK, docstrings are handled by another rule
|
||||
|
||||
|
||||
def buzz():
|
||||
print("buzz") # OK, not in stub file
|
||||
print("buzz") # ERROR PYI010
|
||||
|
||||
|
||||
def foo2():
|
||||
123 # OK, not in a stub file
|
||||
123 # ERROR PYI010
|
||||
|
||||
|
||||
def bizz():
|
||||
x = 123 # OK, not in a stub file
|
||||
x = 123 # ERROR PYI010
|
||||
|
||||
|
||||
def foo3():
|
||||
pass # OK, pass is handled by another rule
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
def bar(): ... # OK
|
||||
def foo():
|
||||
"""foo""" # OK, strings are handled by another rule
|
||||
"""foo""" # OK, docstrings are handled by another rule
|
||||
|
||||
def buzz():
|
||||
print("buzz") # ERROR PYI010
|
||||
@@ -10,3 +10,6 @@ def foo2():
|
||||
|
||||
def bizz():
|
||||
x = 123 # ERROR PYI010
|
||||
|
||||
def foo3():
|
||||
pass # OK, pass is handled by another rule
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
def bar(): # OK
|
||||
...
|
||||
def bar():
|
||||
... # OK
|
||||
|
||||
|
||||
def oof(): # OK, docstrings are handled by another rule
|
||||
def bar():
|
||||
pass # OK
|
||||
|
||||
|
||||
def bar():
|
||||
"""oof""" # OK
|
||||
|
||||
|
||||
def oof(): # ERROR PYI048
|
||||
"""oof"""
|
||||
print("foo")
|
||||
|
||||
|
||||
def foo(): # Ok not in Stub file
|
||||
def foo(): # ERROR PYI048
|
||||
"""foo"""
|
||||
print("foo")
|
||||
print("foo")
|
||||
|
||||
|
||||
def buzz(): # Ok not in Stub file
|
||||
def buzz(): # ERROR PYI048
|
||||
print("fizz")
|
||||
print("buzz")
|
||||
print("test")
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
def bar(): ... # OK
|
||||
def bar():
|
||||
... # OK
|
||||
pass # OK
|
||||
|
||||
def bar():
|
||||
"""oof""" # OK
|
||||
|
||||
def oof(): # OK, docstrings are handled by another rule
|
||||
"""oof"""
|
||||
print("foo")
|
||||
def oof(): # ERROR PYI048
|
||||
"""oof"""
|
||||
print("foo")
|
||||
|
||||
|
||||
|
||||
def foo(): # ERROR PYI048
|
||||
def foo(): # ERROR PYI048
|
||||
"""foo"""
|
||||
print("foo")
|
||||
print("foo")
|
||||
|
||||
|
||||
def buzz(): # ERROR PYI048
|
||||
def buzz(): # ERROR PYI048
|
||||
print("fizz")
|
||||
print("buzz")
|
||||
print("test")
|
||||
|
||||
@@ -4,17 +4,15 @@ use std::collections::BTreeSet;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel, SourceMap};
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::autofix::source_map::SourceMap;
|
||||
use crate::linter::FixTable;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
|
||||
pub(crate) mod codemods;
|
||||
pub(crate) mod edits;
|
||||
pub(crate) mod snippet;
|
||||
pub(crate) mod source_map;
|
||||
|
||||
pub(crate) struct FixResult {
|
||||
/// 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
|
||||
mod tests {
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, SourceMarker};
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::autofix::source_map::SourceMarker;
|
||||
use crate::autofix::{apply_fixes, FixResult};
|
||||
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
|
||||
|
||||
@@ -207,14 +204,8 @@ print("hello world")
|
||||
assert_eq!(
|
||||
source_map.markers(),
|
||||
&[
|
||||
SourceMarker {
|
||||
source: 10.into(),
|
||||
dest: 10.into(),
|
||||
},
|
||||
SourceMarker {
|
||||
source: 10.into(),
|
||||
dest: 21.into(),
|
||||
},
|
||||
SourceMarker::new(10.into(), 10.into(),),
|
||||
SourceMarker::new(10.into(), 21.into(),),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -250,14 +241,8 @@ class A(Bar):
|
||||
assert_eq!(
|
||||
source_map.markers(),
|
||||
&[
|
||||
SourceMarker {
|
||||
source: 8.into(),
|
||||
dest: 8.into(),
|
||||
},
|
||||
SourceMarker {
|
||||
source: 14.into(),
|
||||
dest: 11.into(),
|
||||
},
|
||||
SourceMarker::new(8.into(), 8.into(),),
|
||||
SourceMarker::new(14.into(), 11.into(),),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -289,14 +274,8 @@ class A:
|
||||
assert_eq!(
|
||||
source_map.markers(),
|
||||
&[
|
||||
SourceMarker {
|
||||
source: 7.into(),
|
||||
dest: 7.into()
|
||||
},
|
||||
SourceMarker {
|
||||
source: 15.into(),
|
||||
dest: 7.into()
|
||||
}
|
||||
SourceMarker::new(7.into(), 7.into()),
|
||||
SourceMarker::new(15.into(), 7.into()),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -332,22 +311,10 @@ class A(object):
|
||||
assert_eq!(
|
||||
source_map.markers(),
|
||||
&[
|
||||
SourceMarker {
|
||||
source: 8.into(),
|
||||
dest: 8.into()
|
||||
},
|
||||
SourceMarker {
|
||||
source: 16.into(),
|
||||
dest: 8.into()
|
||||
},
|
||||
SourceMarker {
|
||||
source: 22.into(),
|
||||
dest: 14.into(),
|
||||
},
|
||||
SourceMarker {
|
||||
source: 30.into(),
|
||||
dest: 14.into(),
|
||||
}
|
||||
SourceMarker::new(8.into(), 8.into()),
|
||||
SourceMarker::new(16.into(), 8.into()),
|
||||
SourceMarker::new(22.into(), 14.into(),),
|
||||
SourceMarker::new(30.into(), 14.into(),),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -382,14 +349,8 @@ class A:
|
||||
assert_eq!(
|
||||
source_map.markers(),
|
||||
&[
|
||||
SourceMarker {
|
||||
source: 7.into(),
|
||||
dest: 7.into(),
|
||||
},
|
||||
SourceMarker {
|
||||
source: 15.into(),
|
||||
dest: 7.into(),
|
||||
}
|
||||
SourceMarker::new(7.into(), 7.into(),),
|
||||
SourceMarker::new(15.into(), 7.into(),),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! [Ruff]: https://github.com/astral-sh/ruff
|
||||
|
||||
pub use rule_selector::RuleSelector;
|
||||
pub use rules::pycodestyle::rules::IOError;
|
||||
pub use rules::pycodestyle::rules::{IOError, SyntaxError};
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -20,7 +20,6 @@ mod doc_lines;
|
||||
mod docstrings;
|
||||
pub mod fs;
|
||||
mod importer;
|
||||
pub mod jupyter;
|
||||
mod lex;
|
||||
pub mod line_width;
|
||||
pub mod linter;
|
||||
|
||||
@@ -6,8 +6,6 @@ use anyhow::{anyhow, Result};
|
||||
use colored::Colorize;
|
||||
use itertools::Itertools;
|
||||
use log::error;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::{AsMode, ParseError};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
@@ -15,7 +13,8 @@ use ruff_python_ast::imports::ImportMap;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::{AsMode, ParseError};
|
||||
use ruff_source_file::{Locator, SourceFileBuilder};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -609,3 +608,133 @@ This indicates a bug in `{}`. If you could open an issue at:
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use test_case::test_case;
|
||||
|
||||
use ruff_notebook::{Notebook, NotebookError};
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::source_kind::SourceKind;
|
||||
use crate::test::{test_contents, test_notebook_path, TestedNotebook};
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
/// Construct a path to a Jupyter notebook in the `resources/test/fixtures/jupyter` directory.
|
||||
fn notebook_path(path: impl AsRef<Path>) -> std::path::PathBuf {
|
||||
Path::new("../ruff_notebook/resources/test/fixtures/jupyter").join(path)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_import_sorting() -> Result<(), NotebookError> {
|
||||
let actual = notebook_path("isort.ipynb");
|
||||
let expected = notebook_path("isort_expected.ipynb");
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
&actual,
|
||||
expected,
|
||||
&settings::Settings::for_rule(Rule::UnsortedImports),
|
||||
)?;
|
||||
assert_messages!(messages, actual, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ipy_escape_command() -> Result<(), NotebookError> {
|
||||
let actual = notebook_path("ipy_escape_command.ipynb");
|
||||
let expected = notebook_path("ipy_escape_command_expected.ipynb");
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
&actual,
|
||||
expected,
|
||||
&settings::Settings::for_rule(Rule::UnusedImport),
|
||||
)?;
|
||||
assert_messages!(messages, actual, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unused_variable() -> Result<(), NotebookError> {
|
||||
let actual = notebook_path("unused_variable.ipynb");
|
||||
let expected = notebook_path("unused_variable_expected.ipynb");
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
&actual,
|
||||
expected,
|
||||
&settings::Settings::for_rule(Rule::UnusedVariable),
|
||||
)?;
|
||||
assert_messages!(messages, actual, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_consistency() -> Result<()> {
|
||||
let actual_path = notebook_path("before_fix.ipynb");
|
||||
let expected_path = notebook_path("after_fix.ipynb");
|
||||
|
||||
let TestedNotebook {
|
||||
linted_notebook: fixed_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
actual_path,
|
||||
&expected_path,
|
||||
&settings::Settings::for_rule(Rule::UnusedImport),
|
||||
)?;
|
||||
let mut writer = Vec::new();
|
||||
fixed_notebook.write(&mut writer)?;
|
||||
let actual = String::from_utf8(writer)?;
|
||||
let expected = std::fs::read_to_string(expected_path)?;
|
||||
assert_eq!(actual, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("before_fix.ipynb"), true; "trailing_newline")]
|
||||
#[test_case(Path::new("no_trailing_newline.ipynb"), false; "no_trailing_newline")]
|
||||
fn test_trailing_newline(path: &Path, trailing_newline: bool) -> Result<()> {
|
||||
let notebook = Notebook::from_path(¬ebook_path(path))?;
|
||||
assert_eq!(notebook.trailing_newline(), trailing_newline);
|
||||
|
||||
let mut writer = Vec::new();
|
||||
notebook.write(&mut writer)?;
|
||||
let string = String::from_utf8(writer)?;
|
||||
assert_eq!(string.ends_with('\n'), trailing_newline);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Version <4.5, don't emit cell ids
|
||||
#[test_case(Path::new("no_cell_id.ipynb"), false; "no_cell_id")]
|
||||
// Version 4.5, cell ids are missing and need to be added
|
||||
#[test_case(Path::new("add_missing_cell_id.ipynb"), true; "add_missing_cell_id")]
|
||||
fn test_cell_id(path: &Path, has_id: bool) -> Result<()> {
|
||||
let source_notebook = Notebook::from_path(¬ebook_path(path))?;
|
||||
let source_kind = SourceKind::IpyNotebook(source_notebook);
|
||||
let (_, transformed) = test_contents(
|
||||
&source_kind,
|
||||
path,
|
||||
&settings::Settings::for_rule(Rule::UnusedImport),
|
||||
);
|
||||
let linted_notebook = transformed.into_owned().expect_ipy_notebook();
|
||||
let mut writer = Vec::new();
|
||||
linted_notebook.write(&mut writer)?;
|
||||
let actual = String::from_utf8(writer)?;
|
||||
if has_id {
|
||||
assert!(actual.contains(r#""id": ""#));
|
||||
} else {
|
||||
assert!(!actual.contains(r#""id":"#));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ use ruff_python_parser::{ParseError, ParseErrorType};
|
||||
use ruff_source_file::{OneIndexed, SourceCode, SourceLocation};
|
||||
|
||||
use crate::fs;
|
||||
use crate::jupyter::Notebook;
|
||||
use crate::source_kind::SourceKind;
|
||||
use ruff_notebook::Notebook;
|
||||
|
||||
pub static WARNINGS: Lazy<Mutex<Vec<&'static str>>> = Lazy::new(Mutex::default);
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ use std::num::NonZeroUsize;
|
||||
|
||||
use colored::Colorize;
|
||||
|
||||
use ruff_notebook::{Notebook, NotebookIndex};
|
||||
use ruff_source_file::OneIndexed;
|
||||
|
||||
use crate::fs::relativize_path;
|
||||
use crate::jupyter::{Notebook, NotebookIndex};
|
||||
use crate::message::diff::calculate_print_width;
|
||||
use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
|
||||
use crate::message::{
|
||||
|
||||
@@ -14,12 +14,11 @@ pub use json_lines::JsonLinesEmitter;
|
||||
pub use junit::JunitEmitter;
|
||||
pub use pylint::PylintEmitter;
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_source_file::{SourceFile, SourceLocation};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
pub use text::TextEmitter;
|
||||
|
||||
use crate::jupyter::Notebook;
|
||||
|
||||
mod azure;
|
||||
mod diff;
|
||||
mod github;
|
||||
|
||||
@@ -7,11 +7,11 @@ use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, Sou
|
||||
use bitflags::bitflags;
|
||||
use colored::Colorize;
|
||||
|
||||
use ruff_notebook::{Notebook, NotebookIndex};
|
||||
use ruff_source_file::{OneIndexed, SourceLocation};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::fs::relativize_path;
|
||||
use crate::jupyter::{Notebook, NotebookIndex};
|
||||
use crate::line_width::{LineWidthBuilder, TabSize};
|
||||
use crate::message::diff::Diff;
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
|
||||
@@ -149,6 +149,17 @@ import os
|
||||
# Content Content Content Content Content Content Content Content Content Content
|
||||
|
||||
# Copyright 2023
|
||||
"#
|
||||
.trim(),
|
||||
&settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]),
|
||||
);
|
||||
assert_messages!(diagnostics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn char_boundary() {
|
||||
let diagnostics = test_snippet(
|
||||
r#"কককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককক
|
||||
"#
|
||||
.trim(),
|
||||
&settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use crate::settings::Settings;
|
||||
|
||||
@@ -33,11 +32,7 @@ pub(crate) fn missing_copyright_notice(
|
||||
}
|
||||
|
||||
// Only search the first 1024 bytes in the file.
|
||||
let contents = if locator.len() < 1024 {
|
||||
locator.contents()
|
||||
} else {
|
||||
locator.up_to(TextSize::from(1024))
|
||||
};
|
||||
let contents = locator.up_to(locator.floor_char_boundary(TextSize::new(1024)));
|
||||
|
||||
// Locate the copyright notice.
|
||||
if let Some(match_) = settings.flake8_copyright.notice_rgx.find(contents) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_copyright/mod.rs
|
||||
---
|
||||
<filename>:1:1: CPY001 Missing copyright notice at top of file
|
||||
|
|
||||
1 | কককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককক
|
||||
| CPY001
|
||||
|
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, Stmt};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_docstring_stmt;
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -23,18 +23,35 @@ impl AlwaysAutofixableViolation for NonEmptyStubBody {
|
||||
|
||||
/// PYI010
|
||||
pub(crate) fn non_empty_stub_body(checker: &mut Checker, body: &[Stmt]) {
|
||||
if let [Stmt::Expr(ast::StmtExpr { value, range: _ })] = body {
|
||||
// Ignore multi-statement bodies (covered by PYI048).
|
||||
let [stmt] = body else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Ignore `pass` statements (covered by PYI009).
|
||||
if stmt.is_pass_stmt() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore docstrings (covered by PYI021).
|
||||
if is_docstring_stmt(stmt) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore `...` (the desired case).
|
||||
if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt {
|
||||
if let Expr::Constant(ast::ExprConstant { value, .. }) = value.as_ref() {
|
||||
if matches!(value, Constant::Ellipsis | Constant::Str(_)) {
|
||||
if value.is_ellipsis() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut diagnostic = Diagnostic::new(NonEmptyStubBody, body[0].range());
|
||||
|
||||
let mut diagnostic = Diagnostic::new(NonEmptyStubBody, stmt.range());
|
||||
if checker.patch(Rule::NonEmptyStubBody) {
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
|
||||
format!("..."),
|
||||
body[0].range(),
|
||||
stmt.range(),
|
||||
)));
|
||||
};
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -22,17 +22,16 @@ impl AlwaysAutofixableViolation for PassStatementStubBody {
|
||||
|
||||
/// PYI009
|
||||
pub(crate) fn pass_statement_stub_body(checker: &mut Checker, body: &[Stmt]) {
|
||||
let [stmt] = body else {
|
||||
let [Stmt::Pass(pass)] = body else {
|
||||
return;
|
||||
};
|
||||
if stmt.is_pass_stmt() {
|
||||
let mut diagnostic = Diagnostic::new(PassStatementStubBody, stmt.range());
|
||||
if checker.patch(Rule::PassStatementStubBody) {
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
format!("..."),
|
||||
stmt.range(),
|
||||
)));
|
||||
};
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(PassStatementStubBody, pass.range());
|
||||
if checker.patch(Rule::PassStatementStubBody) {
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
format!("..."),
|
||||
pass.range(),
|
||||
)));
|
||||
};
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use ruff_python_ast::Stmt;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_docstring_stmt;
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::Stmt;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -17,21 +15,12 @@ impl Violation for StubBodyMultipleStatements {
|
||||
}
|
||||
}
|
||||
|
||||
/// PYI010
|
||||
/// PYI048
|
||||
pub(crate) fn stub_body_multiple_statements(checker: &mut Checker, stmt: &Stmt, body: &[Stmt]) {
|
||||
// If the function body consists of exactly one statement, abort.
|
||||
if body.len() == 1 {
|
||||
return;
|
||||
if body.len() > 1 {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
StubBodyMultipleStatements,
|
||||
stmt.identifier(),
|
||||
));
|
||||
}
|
||||
|
||||
// If the function body consists of exactly two statements, and the first is a
|
||||
// docstring, abort (this is covered by PYI021).
|
||||
if body.len() == 2 && is_docstring_stmt(&body[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
StubBodyMultipleStatements,
|
||||
stmt.identifier(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ PYI010.pyi:6:5: PYI010 [*] Function body must contain only `...`
|
||||
|
|
||||
= help: Replace function body with `...`
|
||||
|
||||
ℹ Fix
|
||||
3 3 | """foo""" # OK, strings are handled by another rule
|
||||
ℹ Suggested fix
|
||||
3 3 | """foo""" # OK, docstrings are handled by another rule
|
||||
4 4 |
|
||||
5 5 | def buzz():
|
||||
6 |- print("buzz") # ERROR PYI010
|
||||
@@ -31,7 +31,7 @@ PYI010.pyi:9:5: PYI010 [*] Function body must contain only `...`
|
||||
|
|
||||
= help: Replace function body with `...`
|
||||
|
||||
ℹ Fix
|
||||
ℹ Suggested fix
|
||||
6 6 | print("buzz") # ERROR PYI010
|
||||
7 7 |
|
||||
8 8 | def foo2():
|
||||
@@ -46,14 +46,19 @@ PYI010.pyi:12:5: PYI010 [*] Function body must contain only `...`
|
||||
11 | def bizz():
|
||||
12 | x = 123 # ERROR PYI010
|
||||
| ^^^^^^^ PYI010
|
||||
13 |
|
||||
14 | def foo3():
|
||||
|
|
||||
= help: Replace function body with `...`
|
||||
|
||||
ℹ Fix
|
||||
ℹ Suggested fix
|
||||
9 9 | 123 # ERROR PYI010
|
||||
10 10 |
|
||||
11 11 | def bizz():
|
||||
12 |- x = 123 # ERROR PYI010
|
||||
12 |+ ... # ERROR PYI010
|
||||
13 13 |
|
||||
14 14 | def foo3():
|
||||
15 15 | pass # OK, pass is handled by another rule
|
||||
|
||||
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI048.pyi:11:5: PYI048 Function body must contain exactly one statement
|
||||
PYI048.pyi:8:5: PYI048 Function body must contain exactly one statement
|
||||
|
|
||||
11 | def foo(): # ERROR PYI048
|
||||
6 | """oof""" # OK
|
||||
7 |
|
||||
8 | def oof(): # ERROR PYI048
|
||||
| ^^^ PYI048
|
||||
12 | """foo"""
|
||||
13 | print("foo")
|
||||
9 | """oof"""
|
||||
10 | print("foo")
|
||||
|
|
||||
|
||||
PYI048.pyi:12:5: PYI048 Function body must contain exactly one statement
|
||||
|
|
||||
10 | print("foo")
|
||||
11 |
|
||||
12 | def foo(): # ERROR PYI048
|
||||
| ^^^ PYI048
|
||||
13 | """foo"""
|
||||
14 | print("foo")
|
||||
|
|
||||
|
||||
PYI048.pyi:17:5: PYI048 Function body must contain exactly one statement
|
||||
|
|
||||
17 | def buzz(): # ERROR PYI048
|
||||
15 | print("foo")
|
||||
16 |
|
||||
17 | def buzz(): # ERROR PYI048
|
||||
| ^^^^ PYI048
|
||||
18 | print("fizz")
|
||||
19 | print("buzz")
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use ruff_python_ast::{self as ast, ElifElseClause, ExceptHandler, MatchCase, Stmt};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
use std::iter::Peekable;
|
||||
use std::slice;
|
||||
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_python_ast::statement_visitor::StatementVisitor;
|
||||
use ruff_python_ast::{self as ast, ElifElseClause, ExceptHandler, MatchCase, Stmt};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::directives::IsortDirectives;
|
||||
use crate::jupyter::Notebook;
|
||||
use crate::rules::isort::helpers;
|
||||
use crate::source_kind::SourceKind;
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ pub(crate) use ambiguous_variable_name::*;
|
||||
pub(crate) use bare_except::*;
|
||||
pub(crate) use compound_statements::*;
|
||||
pub(crate) use doc_line_too_long::*;
|
||||
pub use errors::IOError;
|
||||
pub(crate) use errors::*;
|
||||
pub use errors::{IOError, SyntaxError};
|
||||
pub(crate) use imports::*;
|
||||
|
||||
pub(crate) use invalid_escape_sequence::*;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
source: crates/ruff/src/jupyter/notebook.rs
|
||||
source: crates/ruff/src/linter.rs
|
||||
---
|
||||
isort.ipynb:cell 1:1:1: I001 [*] Import block is un-sorted or un-formatted
|
||||
|
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
source: crates/ruff/src/jupyter/notebook.rs
|
||||
source: crates/ruff/src/linter.rs
|
||||
---
|
||||
ipy_escape_command.ipynb:cell 1:5:8: F401 [*] `os` imported but unused
|
||||
|
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
source: crates/ruff/src/jupyter/notebook.rs
|
||||
source: crates/ruff/src/linter.rs
|
||||
---
|
||||
unused_variable.ipynb:cell 1:2:5: F841 [*] Local variable `foo1` is assigned to but never used
|
||||
|
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::autofix::source_map::SourceMap;
|
||||
use crate::jupyter::Notebook;
|
||||
use ruff_diagnostics::SourceMap;
|
||||
use ruff_notebook::Notebook;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, is_macro::Is)]
|
||||
pub enum SourceKind {
|
||||
|
||||
@@ -21,7 +21,6 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::autofix::{fix_file, FixResult};
|
||||
use crate::directives;
|
||||
use crate::jupyter::Notebook;
|
||||
use crate::linter::{check_path, LinterResult};
|
||||
use crate::message::{Emitter, EmitterContext, Message, TextEmitter};
|
||||
use crate::packaging::detect_package_root;
|
||||
@@ -29,18 +28,7 @@ use crate::registry::AsRule;
|
||||
use crate::rules::pycodestyle::rules::syntax_error;
|
||||
use crate::settings::{flags, Settings};
|
||||
use crate::source_kind::SourceKind;
|
||||
|
||||
#[cfg(not(fuzzing))]
|
||||
pub(crate) fn read_jupyter_notebook(path: &Path) -> Result<Notebook> {
|
||||
let path = test_resource_path("fixtures/jupyter").join(path);
|
||||
Notebook::from_path(&path).map_err(|err| {
|
||||
anyhow::anyhow!(
|
||||
"Failed to read notebook file `{}`: {:?}",
|
||||
path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
}
|
||||
use ruff_notebook::{Notebook, NotebookError};
|
||||
|
||||
#[cfg(not(fuzzing))]
|
||||
pub(crate) fn test_resource_path(path: impl AsRef<Path>) -> std::path::PathBuf {
|
||||
@@ -67,12 +55,12 @@ pub(crate) fn test_notebook_path(
|
||||
path: impl AsRef<Path>,
|
||||
expected: impl AsRef<Path>,
|
||||
settings: &Settings,
|
||||
) -> Result<TestedNotebook> {
|
||||
let source_notebook = read_jupyter_notebook(path.as_ref())?;
|
||||
) -> Result<TestedNotebook, NotebookError> {
|
||||
let source_notebook = Notebook::from_path(path.as_ref())?;
|
||||
|
||||
let source_kind = SourceKind::IpyNotebook(source_notebook);
|
||||
let (messages, transformed) = test_contents(&source_kind, path.as_ref(), settings);
|
||||
let expected_notebook = read_jupyter_notebook(expected.as_ref())?;
|
||||
let expected_notebook = Notebook::from_path(expected.as_ref())?;
|
||||
let linted_notebook = transformed.into_owned().expect_ipy_notebook();
|
||||
|
||||
assert_eq!(
|
||||
@@ -273,12 +261,8 @@ Source with applied fixes:
|
||||
(messages, transformed)
|
||||
}
|
||||
|
||||
fn print_diagnostics(
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
file_path: &Path,
|
||||
source: &SourceKind,
|
||||
) -> String {
|
||||
let filename = file_path.file_name().unwrap().to_string_lossy();
|
||||
fn print_diagnostics(diagnostics: Vec<Diagnostic>, path: &Path, source: &SourceKind) -> String {
|
||||
let filename = path.file_name().unwrap().to_string_lossy();
|
||||
let source_file = SourceFileBuilder::new(filename.as_ref(), source.source_code()).finish();
|
||||
|
||||
let messages: Vec<_> = diagnostics
|
||||
@@ -291,7 +275,7 @@ fn print_diagnostics(
|
||||
.collect();
|
||||
|
||||
if let Some(notebook) = source.notebook() {
|
||||
print_jupyter_messages(&messages, &filename, notebook)
|
||||
print_jupyter_messages(&messages, path, notebook)
|
||||
} else {
|
||||
print_messages(&messages)
|
||||
}
|
||||
@@ -299,7 +283,7 @@ fn print_diagnostics(
|
||||
|
||||
pub(crate) fn print_jupyter_messages(
|
||||
messages: &[Message],
|
||||
filename: &str,
|
||||
path: &Path,
|
||||
notebook: &Notebook,
|
||||
) -> String {
|
||||
let mut output = Vec::new();
|
||||
@@ -312,7 +296,7 @@ pub(crate) fn print_jupyter_messages(
|
||||
&mut output,
|
||||
messages,
|
||||
&EmitterContext::new(&FxHashMap::from_iter([(
|
||||
filename.to_string(),
|
||||
path.file_name().unwrap().to_string_lossy().to_string(),
|
||||
notebook.clone(),
|
||||
)])),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_cli"
|
||||
version = "0.0.286"
|
||||
version = "0.0.287"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -25,6 +25,7 @@ ruff = { path = "../ruff", features = ["clap"] }
|
||||
ruff_cache = { path = "../ruff_cache" }
|
||||
ruff_diagnostics = { path = "../ruff_diagnostics" }
|
||||
ruff_formatter = { path = "../ruff_formatter" }
|
||||
ruff_notebook = { path = "../ruff_notebook" }
|
||||
ruff_macros = { path = "../ruff_macros" }
|
||||
ruff_python_ast = { path = "../ruff_python_ast" }
|
||||
ruff_python_formatter = { path = "../ruff_python_formatter" }
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use crate::ExitStatus;
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
use ruff_workspace::options::Options;
|
||||
|
||||
#[allow(clippy::print_stdout)]
|
||||
pub(crate) fn config(key: Option<&str>) -> ExitStatus {
|
||||
pub(crate) fn config(key: Option<&str>) -> Result<()> {
|
||||
match key {
|
||||
None => print!("{}", Options::metadata()),
|
||||
Some(key) => match Options::metadata().get(key) {
|
||||
None => {
|
||||
println!("Unknown option");
|
||||
return ExitStatus::Error;
|
||||
return Err(anyhow!("Unknown option: {key}"));
|
||||
}
|
||||
Some(entry) => {
|
||||
print!("{entry}");
|
||||
}
|
||||
},
|
||||
}
|
||||
ExitStatus::Success
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#![cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
|
||||
use std::fs::write;
|
||||
use std::fs::{write, File};
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::ops::AddAssign;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
@@ -14,18 +14,19 @@ use filetime::FileTime;
|
||||
use log::{debug, error, warn};
|
||||
use rustc_hash::FxHashMap;
|
||||
use similar::TextDiff;
|
||||
use thiserror::Error;
|
||||
|
||||
use ruff::jupyter::{Cell, Notebook};
|
||||
use ruff::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult};
|
||||
use ruff::logging::DisplayParseError;
|
||||
use ruff::message::Message;
|
||||
use ruff::pyproject_toml::lint_pyproject_toml;
|
||||
use ruff::registry::Rule;
|
||||
use ruff::registry::AsRule;
|
||||
use ruff::settings::{flags, AllSettings, Settings};
|
||||
use ruff::source_kind::SourceKind;
|
||||
use ruff::{fs, IOError};
|
||||
use ruff::{fs, IOError, SyntaxError};
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_macros::CacheKey;
|
||||
use ruff_notebook::{Cell, Notebook, NotebookError};
|
||||
use ruff_python_ast::imports::ImportMap;
|
||||
use ruff_python_ast::{PySourceType, SourceType, TomlSourceType};
|
||||
use ruff_source_file::{LineIndex, SourceCode, SourceFileBuilder};
|
||||
@@ -76,27 +77,39 @@ impl Diagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate [`Diagnostics`] based on an [`io::Error`].
|
||||
pub(crate) fn from_io_error(err: &io::Error, path: &Path, settings: &Settings) -> Self {
|
||||
if settings.rules.enabled(Rule::IOError) {
|
||||
let io_err = Diagnostic::new(
|
||||
IOError {
|
||||
message: err.to_string(),
|
||||
},
|
||||
TextRange::default(),
|
||||
);
|
||||
let dummy = SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();
|
||||
/// Generate [`Diagnostics`] based on a [`SourceExtractionError`].
|
||||
pub(crate) fn from_source_error(
|
||||
err: &SourceExtractionError,
|
||||
path: Option<&Path>,
|
||||
settings: &Settings,
|
||||
) -> Self {
|
||||
let diagnostic = Diagnostic::from(err);
|
||||
if settings.rules.enabled(diagnostic.kind.rule()) {
|
||||
let name = path.map_or_else(|| "-".into(), std::path::Path::to_string_lossy);
|
||||
let dummy = SourceFileBuilder::new(name, "").finish();
|
||||
Self::new(
|
||||
vec![Message::from_diagnostic(io_err, dummy, TextSize::default())],
|
||||
vec![Message::from_diagnostic(
|
||||
diagnostic,
|
||||
dummy,
|
||||
TextSize::default(),
|
||||
)],
|
||||
ImportMap::default(),
|
||||
)
|
||||
} else {
|
||||
warn!(
|
||||
"{}{}{} {err}",
|
||||
"Failed to lint ".bold(),
|
||||
fs::relativize_path(path).bold(),
|
||||
":".bold()
|
||||
);
|
||||
match path {
|
||||
Some(path) => {
|
||||
warn!(
|
||||
"{}{}{} {err}",
|
||||
"Failed to lint ".bold(),
|
||||
fs::relativize_path(path).bold(),
|
||||
":".bold()
|
||||
);
|
||||
}
|
||||
None => {
|
||||
warn!("{}{} {err}", "Failed to lint".bold(), ":".bold());
|
||||
}
|
||||
}
|
||||
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
@@ -121,76 +134,6 @@ impl AddAssign for Diagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a Jupyter Notebook from disk.
|
||||
///
|
||||
/// Returns either an indexed Python Jupyter Notebook or a diagnostic (which is empty if we skip).
|
||||
fn notebook_from_path(path: &Path) -> Result<Notebook, Box<Diagnostics>> {
|
||||
let notebook = match Notebook::from_path(path) {
|
||||
Ok(notebook) => {
|
||||
if !notebook.is_python_notebook() {
|
||||
// Not a python notebook, this could e.g. be an R notebook which we want to just skip.
|
||||
debug!(
|
||||
"Skipping {} because it's not a Python notebook",
|
||||
path.display()
|
||||
);
|
||||
return Err(Box::default());
|
||||
}
|
||||
notebook
|
||||
}
|
||||
Err(diagnostic) => {
|
||||
// Failed to read the jupyter notebook
|
||||
return Err(Box::new(Diagnostics {
|
||||
messages: vec![Message::from_diagnostic(
|
||||
*diagnostic,
|
||||
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish(),
|
||||
TextSize::default(),
|
||||
)],
|
||||
..Diagnostics::default()
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(notebook)
|
||||
}
|
||||
|
||||
/// Parse a Jupyter Notebook from a JSON string.
|
||||
///
|
||||
/// Returns either an indexed Python Jupyter Notebook or a diagnostic (which is empty if we skip).
|
||||
fn notebook_from_source_code(
|
||||
source_code: &str,
|
||||
path: Option<&Path>,
|
||||
) -> Result<Notebook, Box<Diagnostics>> {
|
||||
let notebook = match Notebook::from_source_code(source_code) {
|
||||
Ok(notebook) => {
|
||||
if !notebook.is_python_notebook() {
|
||||
// Not a python notebook, this could e.g. be an R notebook which we want to just skip.
|
||||
if let Some(path) = path {
|
||||
debug!(
|
||||
"Skipping {} because it's not a Python notebook",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
return Err(Box::default());
|
||||
}
|
||||
notebook
|
||||
}
|
||||
Err(diagnostic) => {
|
||||
// Failed to read the jupyter notebook
|
||||
return Err(Box::new(Diagnostics {
|
||||
messages: vec![Message::from_diagnostic(
|
||||
*diagnostic,
|
||||
SourceFileBuilder::new(path.map(Path::to_string_lossy).unwrap_or_default(), "")
|
||||
.finish(),
|
||||
TextSize::default(),
|
||||
)],
|
||||
..Diagnostics::default()
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(notebook)
|
||||
}
|
||||
|
||||
/// Lint the source code at the given `Path`.
|
||||
pub(crate) fn lint_path(
|
||||
path: &Path,
|
||||
@@ -235,12 +178,17 @@ pub(crate) fn lint_path(
|
||||
.iter_enabled()
|
||||
.any(|rule_code| rule_code.lint_source().is_pyproject_toml())
|
||||
{
|
||||
let contents = match std::fs::read_to_string(path) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) => {
|
||||
return Ok(Diagnostics::from_io_error(&err, path, &settings.lib));
|
||||
}
|
||||
};
|
||||
let contents =
|
||||
match std::fs::read_to_string(path).map_err(SourceExtractionError::Io) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) => {
|
||||
return Ok(Diagnostics::from_source_error(
|
||||
&err,
|
||||
Some(path),
|
||||
&settings.lib,
|
||||
));
|
||||
}
|
||||
};
|
||||
let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish();
|
||||
lint_pyproject_toml(source_file, &settings.lib)
|
||||
} else {
|
||||
@@ -257,12 +205,14 @@ pub(crate) fn lint_path(
|
||||
|
||||
// Extract the sources from the file.
|
||||
let LintSource(source_kind) = match LintSource::try_from_path(path, source_type) {
|
||||
Ok(sources) => sources,
|
||||
Err(SourceExtractionError::Io(err)) => {
|
||||
return Ok(Diagnostics::from_io_error(&err, path, &settings.lib));
|
||||
}
|
||||
Err(SourceExtractionError::Diagnostics(diagnostics)) => {
|
||||
return Ok(*diagnostics);
|
||||
Ok(Some(sources)) => sources,
|
||||
Ok(None) => return Ok(Diagnostics::default()),
|
||||
Err(err) => {
|
||||
return Ok(Diagnostics::from_source_error(
|
||||
&err,
|
||||
Some(path),
|
||||
&settings.lib,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -293,7 +243,8 @@ pub(crate) fn lint_path(
|
||||
write(path, transformed.as_bytes())?;
|
||||
}
|
||||
SourceKind::IpyNotebook(notebook) => {
|
||||
notebook.write(path)?;
|
||||
let mut writer = BufWriter::new(File::create(path)?);
|
||||
notebook.write(&mut writer)?;
|
||||
}
|
||||
},
|
||||
flags::FixMode::Diff => {
|
||||
@@ -443,17 +394,13 @@ pub(crate) fn lint_stdin(
|
||||
};
|
||||
|
||||
// Extract the sources from the file.
|
||||
let LintSource(source_kind) =
|
||||
match LintSource::try_from_source_code(contents, path, source_type) {
|
||||
Ok(sources) => sources,
|
||||
Err(SourceExtractionError::Io(err)) => {
|
||||
// SAFETY: An `io::Error` can only occur if we're reading from a path.
|
||||
return Ok(Diagnostics::from_io_error(&err, path.unwrap(), settings));
|
||||
}
|
||||
Err(SourceExtractionError::Diagnostics(diagnostics)) => {
|
||||
return Ok(*diagnostics);
|
||||
}
|
||||
};
|
||||
let LintSource(source_kind) = match LintSource::try_from_source_code(contents, source_type) {
|
||||
Ok(Some(sources)) => sources,
|
||||
Ok(None) => return Ok(Diagnostics::default()),
|
||||
Err(err) => {
|
||||
return Ok(Diagnostics::from_source_error(&err, path, settings));
|
||||
}
|
||||
};
|
||||
|
||||
// Lint the inputs.
|
||||
let (
|
||||
@@ -563,15 +510,16 @@ impl LintSource {
|
||||
fn try_from_path(
|
||||
path: &Path,
|
||||
source_type: PySourceType,
|
||||
) -> Result<LintSource, SourceExtractionError> {
|
||||
) -> Result<Option<LintSource>, SourceExtractionError> {
|
||||
if source_type.is_ipynb() {
|
||||
let notebook = notebook_from_path(path).map_err(SourceExtractionError::Diagnostics)?;
|
||||
let source_kind = SourceKind::IpyNotebook(notebook);
|
||||
Ok(LintSource(source_kind))
|
||||
let notebook = Notebook::from_path(path)?;
|
||||
Ok(notebook
|
||||
.is_python_notebook()
|
||||
.then_some(LintSource(SourceKind::IpyNotebook(notebook))))
|
||||
} else {
|
||||
// This is tested by ruff_cli integration test `unreadable_file`
|
||||
let contents = std::fs::read_to_string(path).map_err(SourceExtractionError::Io)?;
|
||||
Ok(LintSource(SourceKind::Python(contents)))
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
Ok(Some(LintSource(SourceKind::Python(contents))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,48 +528,53 @@ impl LintSource {
|
||||
/// the file path should be used for diagnostics, but not for reading the file from disk.
|
||||
fn try_from_source_code(
|
||||
source_code: String,
|
||||
path: Option<&Path>,
|
||||
source_type: PySourceType,
|
||||
) -> Result<LintSource, SourceExtractionError> {
|
||||
) -> Result<Option<LintSource>, SourceExtractionError> {
|
||||
if source_type.is_ipynb() {
|
||||
let notebook = notebook_from_source_code(&source_code, path)
|
||||
.map_err(SourceExtractionError::Diagnostics)?;
|
||||
let source_kind = SourceKind::IpyNotebook(notebook);
|
||||
Ok(LintSource(source_kind))
|
||||
let notebook = Notebook::from_source_code(&source_code)?;
|
||||
Ok(notebook
|
||||
.is_python_notebook()
|
||||
.then_some(LintSource(SourceKind::IpyNotebook(notebook))))
|
||||
} else {
|
||||
Ok(LintSource(SourceKind::Python(source_code)))
|
||||
Ok(Some(LintSource(SourceKind::Python(source_code))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum SourceExtractionError {
|
||||
#[derive(Error, Debug)]
|
||||
pub(crate) enum SourceExtractionError {
|
||||
/// The extraction failed due to an [`io::Error`].
|
||||
Io(io::Error),
|
||||
/// The extraction failed, and generated [`Diagnostics`] to report.
|
||||
Diagnostics(Box<Diagnostics>),
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
/// The extraction failed due to a [`NotebookError`].
|
||||
#[error(transparent)]
|
||||
Notebook(#[from] NotebookError),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use crate::diagnostics::{notebook_from_path, notebook_from_source_code, Diagnostics};
|
||||
|
||||
#[test]
|
||||
fn test_r() {
|
||||
let path = Path::new("../ruff/resources/test/fixtures/jupyter/R.ipynb");
|
||||
// No diagnostics is used as skip signal.
|
||||
assert_eq!(
|
||||
notebook_from_path(path).unwrap_err(),
|
||||
Box::<Diagnostics>::default()
|
||||
);
|
||||
|
||||
let contents = std::fs::read_to_string(path).unwrap();
|
||||
// No diagnostics is used as skip signal.
|
||||
assert_eq!(
|
||||
notebook_from_source_code(&contents, Some(path)).unwrap_err(),
|
||||
Box::<Diagnostics>::default()
|
||||
);
|
||||
impl From<&SourceExtractionError> for Diagnostic {
|
||||
fn from(err: &SourceExtractionError) -> Self {
|
||||
match err {
|
||||
// IO errors.
|
||||
SourceExtractionError::Io(_)
|
||||
| SourceExtractionError::Notebook(NotebookError::Io(_) | NotebookError::Json(_)) => {
|
||||
Diagnostic::new(
|
||||
IOError {
|
||||
message: err.to_string(),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
}
|
||||
// Syntax errors.
|
||||
SourceExtractionError::Notebook(
|
||||
NotebookError::InvalidJson(_)
|
||||
| NotebookError::InvalidSchema(_)
|
||||
| NotebookError::InvalidFormat(_),
|
||||
) => Diagnostic::new(
|
||||
SyntaxError {
|
||||
message: err.to_string(),
|
||||
},
|
||||
TextRange::default(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,18 +139,27 @@ quoting the executed command, along with the relevant file contents and `pyproje
|
||||
if let Some(rule) = rule {
|
||||
commands::rule::rule(rule, format)?;
|
||||
}
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
Command::Config { option } => {
|
||||
commands::config::config(option.as_deref())?;
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
Command::Linter { format } => {
|
||||
commands::linter::linter(format)?;
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
Command::Clean => {
|
||||
commands::clean::clean(log_level)?;
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
Command::Config { option } => return Ok(commands::config::config(option.as_deref())),
|
||||
Command::Linter { format } => commands::linter::linter(format)?,
|
||||
Command::Clean => commands::clean::clean(log_level)?,
|
||||
Command::GenerateShellCompletion { shell } => {
|
||||
shell.generate(&mut Args::command(), &mut stdout());
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
Command::Check(args) => return check(args, log_level),
|
||||
Command::Format(args) => return format(args, log_level),
|
||||
Command::Check(args) => check(args, log_level),
|
||||
Command::Format(args) => format(args, log_level),
|
||||
}
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
fn format(args: FormatCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
|
||||
@@ -18,6 +18,7 @@ ruff_formatter = { path = "../ruff_formatter" }
|
||||
ruff_python_ast = { path = "../ruff_python_ast" }
|
||||
ruff_python_codegen = { path = "../ruff_python_codegen" }
|
||||
ruff_python_formatter = { path = "../ruff_python_formatter" }
|
||||
ruff_notebook = { path = "../ruff_notebook" }
|
||||
ruff_python_literal = { path = "../ruff_python_literal" }
|
||||
ruff_python_parser = { path = "../ruff_python_parser" }
|
||||
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
|
||||
|
||||
@@ -6,7 +6,6 @@ use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use ruff::jupyter;
|
||||
use ruff_python_codegen::round_trip;
|
||||
use ruff_python_stdlib::path::is_jupyter_notebook;
|
||||
|
||||
@@ -20,7 +19,7 @@ pub(crate) struct Args {
|
||||
pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
let path = args.file.as_path();
|
||||
if is_jupyter_notebook(path) {
|
||||
println!("{}", jupyter::round_trip(path)?);
|
||||
println!("{}", ruff_notebook::round_trip(path)?);
|
||||
} else {
|
||||
let contents = fs::read_to_string(&args.file)?;
|
||||
println!("{}", round_trip(&contents, &args.file.to_string_lossy())?);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
pub use diagnostic::{Diagnostic, DiagnosticKind};
|
||||
pub use edit::Edit;
|
||||
pub use fix::{Applicability, Fix, IsolationLevel};
|
||||
pub use source_map::{SourceMap, SourceMarker};
|
||||
pub use violation::{AlwaysAutofixableViolation, AutofixKind, Violation};
|
||||
|
||||
mod diagnostic;
|
||||
mod edit;
|
||||
mod fix;
|
||||
mod source_map;
|
||||
mod violation;
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use crate::Edit;
|
||||
|
||||
/// Lightweight sourcemap marker representing the source and destination
|
||||
/// position for an [`Edit`].
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct SourceMarker {
|
||||
pub struct SourceMarker {
|
||||
/// Position of the marker in the original source.
|
||||
pub(crate) source: TextSize,
|
||||
source: TextSize,
|
||||
/// Position of the marker in the transformed code.
|
||||
pub(crate) dest: TextSize,
|
||||
dest: TextSize,
|
||||
}
|
||||
|
||||
impl SourceMarker {
|
||||
pub fn new(source: TextSize, dest: TextSize) -> Self {
|
||||
Self { source, dest }
|
||||
}
|
||||
|
||||
pub const fn source(&self) -> TextSize {
|
||||
self.source
|
||||
}
|
||||
|
||||
pub const fn dest(&self) -> TextSize {
|
||||
self.dest
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of [`SourceMarker`].
|
||||
@@ -18,12 +32,12 @@ pub(crate) struct SourceMarker {
|
||||
/// the transformed code. Here, only the boundaries of edits are tracked instead
|
||||
/// of every single character.
|
||||
#[derive(Default, PartialEq, Eq)]
|
||||
pub(crate) struct SourceMap(Vec<SourceMarker>);
|
||||
pub struct SourceMap(Vec<SourceMarker>);
|
||||
|
||||
impl SourceMap {
|
||||
/// Returns a slice of all the markers in the sourcemap in the order they
|
||||
/// were added.
|
||||
pub(crate) fn markers(&self) -> &[SourceMarker] {
|
||||
pub fn markers(&self) -> &[SourceMarker] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
@@ -31,7 +45,7 @@ impl SourceMap {
|
||||
///
|
||||
/// The `output_length` is the length of the transformed string before the
|
||||
/// edit is applied.
|
||||
pub(crate) fn push_start_marker(&mut self, edit: &Edit, output_length: TextSize) {
|
||||
pub fn push_start_marker(&mut self, edit: &Edit, output_length: TextSize) {
|
||||
self.0.push(SourceMarker {
|
||||
source: edit.start(),
|
||||
dest: output_length,
|
||||
@@ -42,7 +56,7 @@ impl SourceMap {
|
||||
///
|
||||
/// The `output_length` is the length of the transformed string after the
|
||||
/// edit has been applied.
|
||||
pub(crate) fn push_end_marker(&mut self, edit: &Edit, output_length: TextSize) {
|
||||
pub fn push_end_marker(&mut self, edit: &Edit, output_length: TextSize) {
|
||||
if edit.is_insertion() {
|
||||
self.0.push(SourceMarker {
|
||||
source: edit.start(),
|
||||
31
crates/ruff_notebook/Cargo.toml
Normal file
31
crates/ruff_notebook/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "ruff_notebook"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[lib]
|
||||
|
||||
[dependencies]
|
||||
ruff_diagnostics = { path = "../ruff_diagnostics" }
|
||||
ruff_source_file = { path = "../ruff_source_file" }
|
||||
ruff_text_size = { path = "../ruff_text_size" }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { version = "3.0.0" }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { workspace = true }
|
||||
test-case = { workspace = true }
|
||||
@@ -1,29 +1,23 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::Display;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::iter;
|
||||
use std::io::{BufReader, Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::path::Path;
|
||||
use std::{io, iter};
|
||||
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::Serialize;
|
||||
use serde_json::error::Category;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_parser::lexer::lex;
|
||||
use ruff_python_parser::Mode;
|
||||
use ruff_diagnostics::{SourceMap, SourceMarker};
|
||||
use ruff_source_file::{NewlineWithTrailingNewline, UniversalNewlineIterator};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
use crate::autofix::source_map::{SourceMap, SourceMarker};
|
||||
use crate::jupyter::index::NotebookIndex;
|
||||
use crate::jupyter::schema::{Cell, RawNotebook, SortAlphabetically, SourceValue};
|
||||
use crate::rules::pycodestyle::rules::SyntaxError;
|
||||
use crate::IOError;
|
||||
|
||||
pub const JUPYTER_NOTEBOOK_EXT: &str = "ipynb";
|
||||
use crate::index::NotebookIndex;
|
||||
use crate::schema::{Cell, RawNotebook, SortAlphabetically, SourceValue};
|
||||
|
||||
/// Run round-trip source code generation on a given Jupyter notebook file path.
|
||||
pub fn round_trip(path: &Path) -> anyhow::Result<String> {
|
||||
@@ -37,7 +31,7 @@ pub fn round_trip(path: &Path) -> anyhow::Result<String> {
|
||||
let code = notebook.source_code().to_string();
|
||||
notebook.update_cell_content(&code);
|
||||
let mut writer = Vec::new();
|
||||
notebook.write_inner(&mut writer)?;
|
||||
notebook.write(&mut writer)?;
|
||||
Ok(String::from_utf8(writer)?)
|
||||
}
|
||||
|
||||
@@ -96,6 +90,21 @@ impl Cell {
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that can occur while deserializing a Jupyter Notebook.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NotebookError {
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error(transparent)]
|
||||
Json(serde_json::Error),
|
||||
#[error("Expected a Jupyter Notebook, which must be internally stored as JSON, but this file isn't valid JSON: {0}")]
|
||||
InvalidJson(serde_json::Error),
|
||||
#[error("This file does not match the schema expected of Jupyter Notebooks: {0}")]
|
||||
InvalidSchema(serde_json::Error),
|
||||
#[error("Expected Jupyter Notebook format 4, found: {0}")]
|
||||
InvalidFormat(i64),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Notebook {
|
||||
/// Python source code of the notebook.
|
||||
@@ -121,19 +130,12 @@ pub struct Notebook {
|
||||
|
||||
impl Notebook {
|
||||
/// Read the Jupyter Notebook from the given [`Path`].
|
||||
pub fn from_path(path: &Path) -> Result<Self, Box<Diagnostic>> {
|
||||
Self::from_reader(BufReader::new(File::open(path).map_err(|err| {
|
||||
Diagnostic::new(
|
||||
IOError {
|
||||
message: format!("{err}"),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
})?))
|
||||
pub fn from_path(path: &Path) -> Result<Self, NotebookError> {
|
||||
Self::from_reader(BufReader::new(File::open(path)?))
|
||||
}
|
||||
|
||||
/// Read the Jupyter Notebook from its JSON string.
|
||||
pub fn from_source_code(source_code: &str) -> Result<Self, Box<Diagnostic>> {
|
||||
pub fn from_source_code(source_code: &str) -> Result<Self, NotebookError> {
|
||||
Self::from_reader(Cursor::new(source_code))
|
||||
}
|
||||
|
||||
@@ -141,7 +143,7 @@ impl Notebook {
|
||||
///
|
||||
/// See also the black implementation
|
||||
/// <https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#L1017-L1046>
|
||||
fn from_reader<R>(mut reader: R) -> Result<Self, Box<Diagnostic>>
|
||||
fn from_reader<R>(mut reader: R) -> Result<Self, NotebookError>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
@@ -149,95 +151,27 @@ impl Notebook {
|
||||
let mut buf = [0; 1];
|
||||
reader.read_exact(&mut buf).is_ok_and(|_| buf[0] == b'\n')
|
||||
});
|
||||
reader.rewind().map_err(|err| {
|
||||
Diagnostic::new(
|
||||
IOError {
|
||||
message: format!("{err}"),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
})?;
|
||||
reader.rewind()?;
|
||||
let mut raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
|
||||
Ok(notebook) => notebook,
|
||||
Err(err) => {
|
||||
// Translate the error into a diagnostic
|
||||
return Err(Box::new({
|
||||
match err.classify() {
|
||||
Category::Io => Diagnostic::new(
|
||||
IOError {
|
||||
message: format!("{err}"),
|
||||
},
|
||||
TextRange::default(),
|
||||
),
|
||||
Category::Syntax | Category::Eof => {
|
||||
// Maybe someone saved the python sources (those with the `# %%` separator)
|
||||
// as jupyter notebook instead. Let's help them.
|
||||
let mut contents = String::new();
|
||||
reader
|
||||
.rewind()
|
||||
.and_then(|_| reader.read_to_string(&mut contents))
|
||||
.map_err(|err| {
|
||||
Diagnostic::new(
|
||||
IOError {
|
||||
message: format!("{err}"),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Check if tokenizing was successful and the file is non-empty
|
||||
if lex(&contents, Mode::Module).any(|result| result.is_err()) {
|
||||
Diagnostic::new(
|
||||
SyntaxError {
|
||||
message: format!(
|
||||
"A Jupyter Notebook (.{JUPYTER_NOTEBOOK_EXT}) must internally be JSON, \
|
||||
but this file isn't valid JSON: {err}"
|
||||
),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
} else {
|
||||
Diagnostic::new(
|
||||
SyntaxError {
|
||||
message: format!(
|
||||
"Expected a Jupyter Notebook (.{JUPYTER_NOTEBOOK_EXT}), \
|
||||
which must be internally stored as JSON, \
|
||||
but found a Python source file: {err}"
|
||||
),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Category::Data => {
|
||||
// We could try to read the schema version here but if this fails it's
|
||||
// a bug anyway
|
||||
Diagnostic::new(
|
||||
SyntaxError {
|
||||
message: format!(
|
||||
"This file does not match the schema expected of Jupyter Notebooks: {err}"
|
||||
),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
}
|
||||
return Err(match err.classify() {
|
||||
Category::Io => NotebookError::Json(err),
|
||||
Category::Syntax | Category::Eof => NotebookError::InvalidJson(err),
|
||||
Category::Data => {
|
||||
// We could try to read the schema version here but if this fails it's
|
||||
// a bug anyway.
|
||||
NotebookError::InvalidSchema(err)
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// v4 is what everybody uses
|
||||
if raw_notebook.nbformat != 4 {
|
||||
// bail because we should have already failed at the json schema stage
|
||||
return Err(Box::new(Diagnostic::new(
|
||||
SyntaxError {
|
||||
message: format!(
|
||||
"Expected Jupyter Notebook format 4, found {}",
|
||||
raw_notebook.nbformat
|
||||
),
|
||||
},
|
||||
TextRange::default(),
|
||||
)));
|
||||
return Err(NotebookError::InvalidFormat(raw_notebook.nbformat));
|
||||
}
|
||||
|
||||
let valid_code_cells = raw_notebook
|
||||
@@ -304,13 +238,13 @@ impl Notebook {
|
||||
// The first offset is always going to be at 0, so skip it.
|
||||
for offset in self.cell_offsets.iter_mut().skip(1).rev() {
|
||||
let closest_marker = match last_marker {
|
||||
Some(marker) if marker.source <= *offset => marker,
|
||||
Some(marker) if marker.source() <= *offset => marker,
|
||||
_ => {
|
||||
let Some(marker) = source_map
|
||||
.markers()
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|m| m.source <= *offset)
|
||||
.find(|marker| marker.source() <= *offset)
|
||||
else {
|
||||
// There are no markers above the current offset, so we can
|
||||
// stop here.
|
||||
@@ -321,9 +255,9 @@ impl Notebook {
|
||||
}
|
||||
};
|
||||
|
||||
match closest_marker.source.cmp(&closest_marker.dest) {
|
||||
Ordering::Less => *offset += closest_marker.dest - closest_marker.source,
|
||||
Ordering::Greater => *offset -= closest_marker.source - closest_marker.dest,
|
||||
match closest_marker.source().cmp(&closest_marker.dest()) {
|
||||
Ordering::Less => *offset += closest_marker.dest() - closest_marker.source(),
|
||||
Ordering::Greater => *offset -= closest_marker.source() - closest_marker.dest(),
|
||||
Ordering::Equal => (),
|
||||
}
|
||||
}
|
||||
@@ -431,18 +365,23 @@ impl Notebook {
|
||||
/// The index is built only once when required. This is only used to
|
||||
/// report diagnostics, so by that time all of the autofixes must have
|
||||
/// been applied if `--fix` was passed.
|
||||
pub(crate) fn index(&self) -> &NotebookIndex {
|
||||
pub fn index(&self) -> &NotebookIndex {
|
||||
self.index.get_or_init(|| self.build_index())
|
||||
}
|
||||
|
||||
/// Return the cell offsets for the concatenated source code corresponding
|
||||
/// the Jupyter notebook.
|
||||
pub(crate) fn cell_offsets(&self) -> &[TextSize] {
|
||||
pub fn cell_offsets(&self) -> &[TextSize] {
|
||||
&self.cell_offsets
|
||||
}
|
||||
|
||||
/// Return `true` if the notebook has a trailing newline, `false` otherwise.
|
||||
pub fn trailing_newline(&self) -> bool {
|
||||
self.trailing_newline
|
||||
}
|
||||
|
||||
/// Update the notebook with the given sourcemap and transformed content.
|
||||
pub(crate) fn update(&mut self, source_map: &SourceMap, transformed: String) {
|
||||
pub fn update(&mut self, source_map: &SourceMap, transformed: String) {
|
||||
// Cell offsets must be updated before updating the cell content as
|
||||
// it depends on the offsets to extract the cell content.
|
||||
self.index.take();
|
||||
@@ -465,7 +404,8 @@ impl Notebook {
|
||||
.map_or(true, |language| language.name == "python")
|
||||
}
|
||||
|
||||
fn write_inner(&self, writer: &mut impl Write) -> anyhow::Result<()> {
|
||||
/// Write the notebook back to the given [`Write`] implementor.
|
||||
pub fn write(&self, writer: &mut dyn Write) -> anyhow::Result<()> {
|
||||
// https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#LL1041
|
||||
let formatter = serde_json::ser::PrettyFormatter::with_indent(b" ");
|
||||
let mut serializer = serde_json::Serializer::with_formatter(writer, formatter);
|
||||
@@ -475,13 +415,6 @@ impl Notebook {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write back with an indent of 1, just like black
|
||||
pub fn write(&self, path: &Path) -> anyhow::Result<()> {
|
||||
let mut writer = BufWriter::new(File::create(path)?);
|
||||
self.write_inner(&mut writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -491,58 +424,41 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::jupyter::index::NotebookIndex;
|
||||
use crate::jupyter::schema::Cell;
|
||||
use crate::jupyter::Notebook;
|
||||
use crate::registry::Rule;
|
||||
use crate::source_kind::SourceKind;
|
||||
use crate::test::{
|
||||
read_jupyter_notebook, test_contents, test_notebook_path, test_resource_path,
|
||||
TestedNotebook,
|
||||
};
|
||||
use crate::{assert_messages, settings};
|
||||
use crate::{Cell, Notebook, NotebookError, NotebookIndex};
|
||||
|
||||
/// Read a Jupyter cell from the `resources/test/fixtures/jupyter/cell` directory.
|
||||
fn read_jupyter_cell(path: impl AsRef<Path>) -> Result<Cell> {
|
||||
let path = test_resource_path("fixtures/jupyter/cell").join(path);
|
||||
let source_code = std::fs::read_to_string(path)?;
|
||||
Ok(serde_json::from_str(&source_code)?)
|
||||
/// Construct a path to a Jupyter notebook in the `resources/test/fixtures/jupyter` directory.
|
||||
fn notebook_path(path: impl AsRef<Path>) -> std::path::PathBuf {
|
||||
Path::new("./resources/test/fixtures/jupyter").join(path)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid() {
|
||||
assert!(read_jupyter_notebook(Path::new("valid.ipynb")).is_ok());
|
||||
fn test_python() -> Result<(), NotebookError> {
|
||||
let notebook = Notebook::from_path(¬ebook_path("valid.ipynb"))?;
|
||||
assert!(notebook.is_python_notebook());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_r() {
|
||||
// We can load this, it will be filtered out later
|
||||
assert!(read_jupyter_notebook(Path::new("R.ipynb")).is_ok());
|
||||
fn test_r() -> Result<(), NotebookError> {
|
||||
let notebook = Notebook::from_path(¬ebook_path("R.ipynb"))?;
|
||||
assert!(!notebook.is_python_notebook());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid() {
|
||||
let path = Path::new("resources/test/fixtures/jupyter/invalid_extension.ipynb");
|
||||
assert_eq!(
|
||||
Notebook::from_path(path).unwrap_err().kind.body,
|
||||
"SyntaxError: Expected a Jupyter Notebook (.ipynb), \
|
||||
which must be internally stored as JSON, \
|
||||
but found a Python source file: \
|
||||
expected value at line 1 column 1"
|
||||
);
|
||||
let path = Path::new("resources/test/fixtures/jupyter/not_json.ipynb");
|
||||
assert_eq!(
|
||||
Notebook::from_path(path).unwrap_err().kind.body,
|
||||
"SyntaxError: A Jupyter Notebook (.ipynb) must internally be JSON, \
|
||||
but this file isn't valid JSON: \
|
||||
expected value at line 1 column 1"
|
||||
);
|
||||
let path = Path::new("resources/test/fixtures/jupyter/wrong_schema.ipynb");
|
||||
assert_eq!(
|
||||
Notebook::from_path(path).unwrap_err().kind.body,
|
||||
"SyntaxError: This file does not match the schema expected of Jupyter Notebooks: \
|
||||
missing field `cells` at line 1 column 2"
|
||||
);
|
||||
assert!(matches!(
|
||||
Notebook::from_path(¬ebook_path("invalid_extension.ipynb")),
|
||||
Err(NotebookError::InvalidJson(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
Notebook::from_path(¬ebook_path("not_json.ipynb")),
|
||||
Err(NotebookError::InvalidJson(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
Notebook::from_path(¬ebook_path("wrong_schema.ipynb")),
|
||||
Err(NotebookError::InvalidSchema(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test_case(Path::new("markdown.json"), false; "markdown")]
|
||||
@@ -551,13 +467,20 @@ mod tests {
|
||||
#[test_case(Path::new("only_code.json"), true; "only_code")]
|
||||
#[test_case(Path::new("cell_magic.json"), false; "cell_magic")]
|
||||
fn test_is_valid_code_cell(path: &Path, expected: bool) -> Result<()> {
|
||||
/// Read a Jupyter cell from the `resources/test/fixtures/jupyter/cell` directory.
|
||||
fn read_jupyter_cell(path: impl AsRef<Path>) -> Result<Cell> {
|
||||
let path = notebook_path("cell").join(path);
|
||||
let source_code = std::fs::read_to_string(path)?;
|
||||
Ok(serde_json::from_str(&source_code)?)
|
||||
}
|
||||
|
||||
assert_eq!(read_jupyter_cell(path)?.is_valid_code_cell(), expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concat_notebook() -> Result<()> {
|
||||
let notebook = read_jupyter_notebook(Path::new("valid.ipynb"))?;
|
||||
fn test_concat_notebook() -> Result<(), NotebookError> {
|
||||
let notebook = Notebook::from_path(¬ebook_path("valid.ipynb"))?;
|
||||
assert_eq!(
|
||||
notebook.source_code,
|
||||
r#"def unused_variable():
|
||||
@@ -597,110 +520,4 @@ print("after empty cells")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_import_sorting() -> Result<()> {
|
||||
let path = "isort.ipynb".to_string();
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
&path,
|
||||
Path::new("isort_expected.ipynb"),
|
||||
&settings::Settings::for_rule(Rule::UnsortedImports),
|
||||
)?;
|
||||
assert_messages!(messages, path, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ipy_escape_command() -> Result<()> {
|
||||
let path = "ipy_escape_command.ipynb".to_string();
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
&path,
|
||||
Path::new("ipy_escape_command_expected.ipynb"),
|
||||
&settings::Settings::for_rule(Rule::UnusedImport),
|
||||
)?;
|
||||
assert_messages!(messages, path, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unused_variable() -> Result<()> {
|
||||
let path = "unused_variable.ipynb".to_string();
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
&path,
|
||||
Path::new("unused_variable_expected.ipynb"),
|
||||
&settings::Settings::for_rule(Rule::UnusedVariable),
|
||||
)?;
|
||||
assert_messages!(messages, path, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_consistency() -> Result<()> {
|
||||
let path = "before_fix.ipynb".to_string();
|
||||
let TestedNotebook {
|
||||
linted_notebook: fixed_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
path,
|
||||
Path::new("after_fix.ipynb"),
|
||||
&settings::Settings::for_rule(Rule::UnusedImport),
|
||||
)?;
|
||||
let mut writer = Vec::new();
|
||||
fixed_notebook.write_inner(&mut writer)?;
|
||||
let actual = String::from_utf8(writer)?;
|
||||
let expected =
|
||||
std::fs::read_to_string(test_resource_path("fixtures/jupyter/after_fix.ipynb"))?;
|
||||
assert_eq!(actual, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("before_fix.ipynb"), true; "trailing_newline")]
|
||||
#[test_case(Path::new("no_trailing_newline.ipynb"), false; "no_trailing_newline")]
|
||||
fn test_trailing_newline(path: &Path, trailing_newline: bool) -> Result<()> {
|
||||
let notebook = read_jupyter_notebook(path)?;
|
||||
assert_eq!(notebook.trailing_newline, trailing_newline);
|
||||
|
||||
let mut writer = Vec::new();
|
||||
notebook.write_inner(&mut writer)?;
|
||||
let string = String::from_utf8(writer)?;
|
||||
assert_eq!(string.ends_with('\n'), trailing_newline);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Version <4.5, don't emit cell ids
|
||||
#[test_case(Path::new("no_cell_id.ipynb"), false; "no_cell_id")]
|
||||
// Version 4.5, cell ids are missing and need to be added
|
||||
#[test_case(Path::new("add_missing_cell_id.ipynb"), true; "add_missing_cell_id")]
|
||||
fn test_cell_id(path: &Path, has_id: bool) -> Result<()> {
|
||||
let source_notebook = read_jupyter_notebook(path)?;
|
||||
let source_kind = SourceKind::IpyNotebook(source_notebook);
|
||||
let (_, transformed) = test_contents(
|
||||
&source_kind,
|
||||
path,
|
||||
&settings::Settings::for_rule(Rule::UnusedImport),
|
||||
);
|
||||
let linted_notebook = transformed.into_owned().expect_ipy_notebook();
|
||||
let mut writer = Vec::new();
|
||||
linted_notebook.write_inner(&mut writer)?;
|
||||
let actual = String::from_utf8(writer)?;
|
||||
if has_id {
|
||||
assert!(actual.contains(r#""id": ""#));
|
||||
} else {
|
||||
assert!(!actual.contains(r#""id":"#));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ fn sort_alphabetically<T: Serialize, S: serde::Serializer>(
|
||||
///
|
||||
/// use serde::Serialize;
|
||||
///
|
||||
/// use ruff::jupyter::SortAlphabetically;
|
||||
/// use ruff_notebook::SortAlphabetically;
|
||||
///
|
||||
/// #[derive(Serialize)]
|
||||
/// struct MyStruct {
|
||||
@@ -2716,7 +2716,7 @@ impl Ranged for crate::nodes::StmtContinue {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
impl Ranged for StmtIpyEscapeCommand {
|
||||
impl Ranged for crate::nodes::StmtIpyEscapeCommand {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
}
|
||||
@@ -2888,7 +2888,7 @@ impl Ranged for crate::nodes::ExprSlice {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
impl Ranged for ExprIpyEscapeCommand {
|
||||
impl Ranged for crate::nodes::ExprIpyEscapeCommand {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
}
|
||||
@@ -2927,7 +2927,6 @@ impl Ranged for crate::Expr {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for crate::nodes::Comprehension {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
@@ -2945,7 +2944,6 @@ impl Ranged for crate::ExceptHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for crate::nodes::Parameter {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
@@ -3086,6 +3084,173 @@ impl Ranged for crate::nodes::ParameterWithDefault {
|
||||
}
|
||||
}
|
||||
|
||||
/// An expression that may be parenthesized.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ParenthesizedExpr {
|
||||
/// The range of the expression, including any parentheses.
|
||||
pub range: TextRange,
|
||||
/// The underlying expression.
|
||||
pub expr: Expr,
|
||||
}
|
||||
impl Ranged for ParenthesizedExpr {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
impl From<Expr> for ParenthesizedExpr {
|
||||
fn from(expr: Expr) -> Self {
|
||||
ParenthesizedExpr {
|
||||
range: expr.range(),
|
||||
expr,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<ParenthesizedExpr> for Expr {
|
||||
fn from(parenthesized_expr: ParenthesizedExpr) -> Self {
|
||||
parenthesized_expr.expr
|
||||
}
|
||||
}
|
||||
impl From<ExprIpyEscapeCommand> for ParenthesizedExpr {
|
||||
fn from(payload: ExprIpyEscapeCommand) -> Self {
|
||||
Expr::IpyEscapeCommand(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprBoolOp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprBoolOp) -> Self {
|
||||
Expr::BoolOp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprNamedExpr> for ParenthesizedExpr {
|
||||
fn from(payload: ExprNamedExpr) -> Self {
|
||||
Expr::NamedExpr(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprBinOp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprBinOp) -> Self {
|
||||
Expr::BinOp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprUnaryOp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprUnaryOp) -> Self {
|
||||
Expr::UnaryOp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprLambda> for ParenthesizedExpr {
|
||||
fn from(payload: ExprLambda) -> Self {
|
||||
Expr::Lambda(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprIfExp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprIfExp) -> Self {
|
||||
Expr::IfExp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprDict> for ParenthesizedExpr {
|
||||
fn from(payload: ExprDict) -> Self {
|
||||
Expr::Dict(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprSet> for ParenthesizedExpr {
|
||||
fn from(payload: ExprSet) -> Self {
|
||||
Expr::Set(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprListComp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprListComp) -> Self {
|
||||
Expr::ListComp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprSetComp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprSetComp) -> Self {
|
||||
Expr::SetComp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprDictComp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprDictComp) -> Self {
|
||||
Expr::DictComp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprGeneratorExp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprGeneratorExp) -> Self {
|
||||
Expr::GeneratorExp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprAwait> for ParenthesizedExpr {
|
||||
fn from(payload: ExprAwait) -> Self {
|
||||
Expr::Await(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprYield> for ParenthesizedExpr {
|
||||
fn from(payload: ExprYield) -> Self {
|
||||
Expr::Yield(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprYieldFrom> for ParenthesizedExpr {
|
||||
fn from(payload: ExprYieldFrom) -> Self {
|
||||
Expr::YieldFrom(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprCompare> for ParenthesizedExpr {
|
||||
fn from(payload: ExprCompare) -> Self {
|
||||
Expr::Compare(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprCall> for ParenthesizedExpr {
|
||||
fn from(payload: ExprCall) -> Self {
|
||||
Expr::Call(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprFormattedValue> for ParenthesizedExpr {
|
||||
fn from(payload: ExprFormattedValue) -> Self {
|
||||
Expr::FormattedValue(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprFString> for ParenthesizedExpr {
|
||||
fn from(payload: ExprFString) -> Self {
|
||||
Expr::FString(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprConstant> for ParenthesizedExpr {
|
||||
fn from(payload: ExprConstant) -> Self {
|
||||
Expr::Constant(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprAttribute> for ParenthesizedExpr {
|
||||
fn from(payload: ExprAttribute) -> Self {
|
||||
Expr::Attribute(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprSubscript> for ParenthesizedExpr {
|
||||
fn from(payload: ExprSubscript) -> Self {
|
||||
Expr::Subscript(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprStarred> for ParenthesizedExpr {
|
||||
fn from(payload: ExprStarred) -> Self {
|
||||
Expr::Starred(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprName> for ParenthesizedExpr {
|
||||
fn from(payload: ExprName) -> Self {
|
||||
Expr::Name(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprList> for ParenthesizedExpr {
|
||||
fn from(payload: ExprList) -> Self {
|
||||
Expr::List(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprTuple> for ParenthesizedExpr {
|
||||
fn from(payload: ExprTuple) -> Self {
|
||||
Expr::Tuple(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprSlice> for ParenthesizedExpr {
|
||||
fn from(payload: ExprSlice) -> Self {
|
||||
Expr::Slice(payload).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
mod size_assertions {
|
||||
use static_assertions::assert_eq_size;
|
||||
|
||||
@@ -33,3 +33,27 @@ result_f = (
|
||||
# comment
|
||||
''
|
||||
)
|
||||
|
||||
(
|
||||
f'{1}' # comment
|
||||
f'{2}'
|
||||
)
|
||||
|
||||
(
|
||||
f'{1}'
|
||||
f'{2}' # comment
|
||||
)
|
||||
|
||||
(
|
||||
1, ( # comment
|
||||
f'{2}'
|
||||
)
|
||||
)
|
||||
|
||||
(
|
||||
(
|
||||
f'{1}'
|
||||
# comment
|
||||
),
|
||||
2
|
||||
)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
def test():
|
||||
# fmt: off
|
||||
a_very_small_indent
|
||||
(
|
||||
not_fixed
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
more
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
def test():
|
||||
a_small_indent
|
||||
# fmt: off
|
||||
# fix under-indented comments
|
||||
(or_the_inner_expression +
|
||||
expressions
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def test():
|
||||
pass
|
||||
|
||||
# It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment
|
||||
# of the `test` function if the "proper" indentation is larger than 2 spaces.
|
||||
# fmt: on
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted;
|
||||
|
||||
def test():
|
||||
pass
|
||||
# fmt: off
|
||||
"""A multiline strings
|
||||
that should not get formatted"""
|
||||
|
||||
"A single quoted multiline \
|
||||
string"
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted;
|
||||
|
||||
36
crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/newlines.py
vendored
Normal file
36
crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/newlines.py
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
def func():
|
||||
pass
|
||||
# fmt: off
|
||||
x = 1
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
# fmt: on
|
||||
x = 1
|
||||
|
||||
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
# fmt: on
|
||||
def func():
|
||||
pass
|
||||
|
||||
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
|
||||
|
||||
# fmt: on
|
||||
def func():
|
||||
pass
|
||||
# fmt: on
|
||||
def func():
|
||||
pass
|
||||
161
crates/ruff_python_formatter/resources/test/fixtures/ruff/newlines.py
vendored
Normal file
161
crates/ruff_python_formatter/resources/test/fixtures/ruff/newlines.py
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
###
|
||||
# Blank lines around functions
|
||||
###
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
if True:
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
|
||||
# comment
|
||||
def f():
|
||||
pass
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
# comment
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
# comment
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
###
|
||||
# Blank lines around imports.
|
||||
###
|
||||
|
||||
def f():
|
||||
import x
|
||||
# comment
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
# comment
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
# comment
|
||||
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
|
||||
# comment
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x # comment
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f(): pass # comment
|
||||
# comment
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
x = 1
|
||||
@@ -1,6 +1,29 @@
|
||||
# Pragma reserved width fixtures
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # nocoverage: This should break
|
||||
|
||||
|
||||
# Pragma fixtures for non-breaking space (lead by NBSP)
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # nocoverage: This should break
|
||||
|
||||
|
||||
# As of adding this fixture Black adds a space before the non-breaking space if part of a type pragma.
|
||||
# https://github.com/psf/black/blob/b4dca26c7d93f930bbd5a7b552807370b60d4298/src/black/comments.py#L122-L129
|
||||
i2 = "" # type: Add space before leading NBSP followed by spaces
|
||||
i3 = "" #type: A space is added
|
||||
i4 = "" # type: Add space before leading NBSP followed by a space
|
||||
i5 = "" # type: Add space before leading NBSP
|
||||
i = "" # type: Add space before leading NBSP followed by spaces
|
||||
i = "" #type: A space is added
|
||||
i = "" # type: Add space before leading NBSP followed by a space
|
||||
i = "" # type: Add space before leading NBSP
|
||||
i = "" # type: Add space before two leading NBSP
|
||||
|
||||
|
||||
# A noqa as `#\u{A0}\u{A0}noqa` becomes `# \u{A0}noqa`
|
||||
i = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # noqa
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::borrow::Cow;
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
use ruff_formatter::{format_args, write, FormatError, FormatOptions, SourceCode};
|
||||
use ruff_python_ast::node::{AnyNodeRef, AstNode};
|
||||
use ruff_python_trivia::{lines_after, lines_after_ignoring_trivia, lines_before};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||
|
||||
use crate::comments::{CommentLinePosition, SourceComment};
|
||||
use crate::context::NodeLevel;
|
||||
@@ -299,10 +299,10 @@ impl Format<PyFormatContext<'_>> for FormatComment<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper that inserts the appropriate number of empty lines before a comment, depending on the node level.
|
||||
// Top level: Up to two empty lines
|
||||
// parenthesized: A single empty line
|
||||
// other: Up to a single empty line
|
||||
/// Helper that inserts the appropriate number of empty lines before a comment, depending on the node level:
|
||||
/// - Top-level: Up to two empty lines.
|
||||
/// - Parenthesized: A single empty line.
|
||||
/// - Otherwise: Up to a single empty line.
|
||||
pub(crate) const fn empty_lines(lines: u32) -> FormatEmptyLines {
|
||||
FormatEmptyLines { lines }
|
||||
}
|
||||
@@ -357,17 +357,33 @@ impl Format<PyFormatContext<'_>> for FormatTrailingEndOfLineComment<'_> {
|
||||
|
||||
let normalized_comment = normalize_comment(self.comment, source)?;
|
||||
|
||||
// Start with 2 because of the two leading spaces.
|
||||
let mut reserved_width = 2;
|
||||
// Trim the normalized comment to detect excluded pragmas (strips NBSP).
|
||||
let trimmed = strip_comment_prefix(&normalized_comment)?.trim_start();
|
||||
|
||||
// SAFE: The formatted file is <= 4GB, and each comment should as well.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
for c in normalized_comment.chars() {
|
||||
reserved_width += match c {
|
||||
'\t' => f.options().tab_width().value(),
|
||||
c => c.width().unwrap_or(0) as u32,
|
||||
let is_pragma = if let Some((maybe_pragma, _)) = trimmed.split_once(':') {
|
||||
matches!(maybe_pragma, "noqa" | "type" | "pyright" | "pylint")
|
||||
} else {
|
||||
trimmed.starts_with("noqa")
|
||||
};
|
||||
|
||||
// Don't reserve width for excluded pragma comments.
|
||||
let reserved_width = if is_pragma {
|
||||
0
|
||||
} else {
|
||||
// Start with 2 because of the two leading spaces.
|
||||
let mut width = 2;
|
||||
|
||||
// SAFETY: The formatted file is <= 4GB, and each comment should as well.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
for c in normalized_comment.chars() {
|
||||
width += match c {
|
||||
'\t' => f.options().tab_width().value(),
|
||||
c => c.width().unwrap_or(0) as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
width
|
||||
};
|
||||
|
||||
write!(
|
||||
f,
|
||||
@@ -442,11 +458,7 @@ fn normalize_comment<'a>(
|
||||
|
||||
let trimmed = comment_text.trim_end();
|
||||
|
||||
let Some(content) = trimmed.strip_prefix('#') else {
|
||||
return Err(FormatError::syntax_error(
|
||||
"Didn't find expected comment token `#`",
|
||||
));
|
||||
};
|
||||
let content = strip_comment_prefix(trimmed)?;
|
||||
|
||||
if content.is_empty() {
|
||||
return Ok(Cow::Borrowed("#"));
|
||||
@@ -462,16 +474,70 @@ fn normalize_comment<'a>(
|
||||
if content.starts_with('\u{A0}') {
|
||||
let trimmed = content.trim_start_matches('\u{A0}');
|
||||
|
||||
// Black adds a space before the non-breaking space if part of a type pragma.
|
||||
if trimmed.trim_start().starts_with("type:") {
|
||||
return Ok(Cow::Owned(std::format!("# \u{A0}{trimmed}")));
|
||||
}
|
||||
|
||||
// Black replaces the non-breaking space with a space if followed by a space.
|
||||
if trimmed.starts_with(' ') {
|
||||
return Ok(Cow::Owned(std::format!("# {trimmed}")));
|
||||
// Black adds a space before the non-breaking space if part of a type pragma.
|
||||
Ok(Cow::Owned(std::format!("# {content}")))
|
||||
} else if trimmed.starts_with(' ') {
|
||||
// Black replaces the non-breaking space with a space if followed by a space.
|
||||
Ok(Cow::Owned(std::format!("# {trimmed}")))
|
||||
} else {
|
||||
// Otherwise we replace the first non-breaking space with a regular space.
|
||||
Ok(Cow::Owned(std::format!("# {}", &content["\u{A0}".len()..])))
|
||||
}
|
||||
} else {
|
||||
Ok(Cow::Owned(std::format!("# {}", content.trim_start())))
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper for stripping '#' from comments.
|
||||
fn strip_comment_prefix(comment_text: &str) -> FormatResult<&str> {
|
||||
let Some(content) = comment_text.strip_prefix('#') else {
|
||||
return Err(FormatError::syntax_error(
|
||||
"Didn't find expected comment token `#`",
|
||||
));
|
||||
};
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Format the empty lines between a node and its trailing comments.
|
||||
///
|
||||
/// For example, given:
|
||||
/// ```python
|
||||
/// def func():
|
||||
/// ...
|
||||
/// # comment
|
||||
/// ```
|
||||
///
|
||||
/// This builder will insert two empty lines before the comment.
|
||||
/// ```
|
||||
pub(crate) const fn empty_lines_before_trailing_comments(
|
||||
comments: &[SourceComment],
|
||||
expected: u32,
|
||||
) -> FormatEmptyLinesBeforeTrailingComments {
|
||||
FormatEmptyLinesBeforeTrailingComments { comments, expected }
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct FormatEmptyLinesBeforeTrailingComments<'a> {
|
||||
/// The trailing comments of the node.
|
||||
comments: &'a [SourceComment],
|
||||
/// The expected number of empty lines before the trailing comments.
|
||||
expected: u32,
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for FormatEmptyLinesBeforeTrailingComments<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<PyFormatContext>) -> FormatResult<()> {
|
||||
if let Some(comment) = self
|
||||
.comments
|
||||
.iter()
|
||||
.find(|comment| comment.line_position().is_own_line())
|
||||
{
|
||||
let actual = lines_before(comment.start(), f.context().source()).saturating_sub(1);
|
||||
for _ in actual..self.expected {
|
||||
write!(f, [empty_line()])?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(Cow::Owned(std::format!("# {}", content.trim_start())))
|
||||
}
|
||||
|
||||
@@ -70,6 +70,20 @@ fn handle_parenthesized_comment<'a>(
|
||||
comment: DecoratedComment<'a>,
|
||||
locator: &Locator,
|
||||
) -> CommentPlacement<'a> {
|
||||
// As a special-case, ignore comments within f-strings, like:
|
||||
// ```python
|
||||
// (
|
||||
// f'{1}' # comment
|
||||
// f'{2}'
|
||||
// )
|
||||
// ```
|
||||
// These can't be parenthesized, as they must fall between two string tokens in an implicit
|
||||
// concatenation. But the expression ranges only include the `1` and `2` above, so we also
|
||||
// can't lex the contents between them.
|
||||
if comment.enclosing_node().is_expr_f_string() {
|
||||
return CommentPlacement::Default(comment);
|
||||
}
|
||||
|
||||
let Some(preceding) = comment.preceding_node() else {
|
||||
return CommentPlacement::Default(comment);
|
||||
};
|
||||
@@ -425,7 +439,7 @@ fn handle_own_line_comment_around_body<'a>(
|
||||
return CommentPlacement::Default(comment);
|
||||
};
|
||||
|
||||
// If there's any non-trivia token between the preceding node and the comment, than it means
|
||||
// If there's any non-trivia token between the preceding node and the comment, then it means
|
||||
// we're past the case of the alternate branch, defer to the default rules
|
||||
// ```python
|
||||
// if a:
|
||||
@@ -446,11 +460,78 @@ fn handle_own_line_comment_around_body<'a>(
|
||||
}
|
||||
|
||||
// Check if we're between bodies and should attach to the following body.
|
||||
handle_own_line_comment_between_branches(comment, preceding, locator).or_else(|comment| {
|
||||
// Otherwise, there's no following branch or the indentation is too deep, so attach to the
|
||||
// recursively last statement in the preceding body with the matching indentation.
|
||||
handle_own_line_comment_after_branch(comment, preceding, locator)
|
||||
})
|
||||
handle_own_line_comment_between_branches(comment, preceding, locator)
|
||||
.or_else(|comment| {
|
||||
// Otherwise, there's no following branch or the indentation is too deep, so attach to the
|
||||
// recursively last statement in the preceding body with the matching indentation.
|
||||
handle_own_line_comment_after_branch(comment, preceding, locator)
|
||||
})
|
||||
.or_else(|comment| handle_own_line_comment_between_statements(comment, locator))
|
||||
}
|
||||
|
||||
/// Handles own-line comments between statements. If an own-line comment is between two statements,
|
||||
/// it's treated as a leading comment of the following statement _if_ there are no empty lines
|
||||
/// separating the comment and the statement; otherwise, it's treated as a trailing comment of the
|
||||
/// preceding statement.
|
||||
///
|
||||
/// For example, this comment would be a trailing comment of `x = 1`:
|
||||
/// ```python
|
||||
/// x = 1
|
||||
/// # comment
|
||||
///
|
||||
/// y = 2
|
||||
/// ```
|
||||
///
|
||||
/// However, this comment would be a leading comment of `y = 2`:
|
||||
/// ```python
|
||||
/// x = 1
|
||||
///
|
||||
/// # comment
|
||||
/// y = 2
|
||||
/// ```
|
||||
fn handle_own_line_comment_between_statements<'a>(
|
||||
comment: DecoratedComment<'a>,
|
||||
locator: &Locator,
|
||||
) -> CommentPlacement<'a> {
|
||||
let Some(preceding) = comment.preceding_node() else {
|
||||
return CommentPlacement::Default(comment);
|
||||
};
|
||||
|
||||
let Some(following) = comment.following_node() else {
|
||||
return CommentPlacement::Default(comment);
|
||||
};
|
||||
|
||||
// We're looking for comments between two statements, like:
|
||||
// ```python
|
||||
// x = 1
|
||||
// # comment
|
||||
// y = 2
|
||||
// ```
|
||||
if !preceding.is_statement() || !following.is_statement() {
|
||||
return CommentPlacement::Default(comment);
|
||||
}
|
||||
|
||||
// If the comment is directly attached to the following statement; make it a leading
|
||||
// comment:
|
||||
// ```python
|
||||
// x = 1
|
||||
//
|
||||
// # leading comment
|
||||
// y = 2
|
||||
// ```
|
||||
//
|
||||
// Otherwise, if there's at least one empty line, make it a trailing comment:
|
||||
// ```python
|
||||
// x = 1
|
||||
// # trailing comment
|
||||
//
|
||||
// y = 2
|
||||
// ```
|
||||
if max_empty_lines(locator.slice(TextRange::new(comment.end(), following.start()))) == 0 {
|
||||
CommentPlacement::leading(following, comment)
|
||||
} else {
|
||||
CommentPlacement::trailing(preceding, comment)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles own line comments between two branches of a node.
|
||||
@@ -1837,6 +1918,7 @@ fn max_empty_lines(contents: &str) -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
max_new_lines = newlines.max(max_new_lines);
|
||||
max_new_lines.saturating_sub(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,13 @@ expression: comments.debug(test_case.source_code)
|
||||
},
|
||||
],
|
||||
"dangling": [],
|
||||
"trailing": [],
|
||||
"trailing": [
|
||||
SourceComment {
|
||||
text: "# own line comment",
|
||||
position: OwnLine,
|
||||
formatted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
Node {
|
||||
kind: StmtIf,
|
||||
@@ -48,19 +54,4 @@ expression: comments.debug(test_case.source_code)
|
||||
"dangling": [],
|
||||
"trailing": [],
|
||||
},
|
||||
Node {
|
||||
kind: StmtExpr,
|
||||
range: 234..246,
|
||||
source: `test(10, 20)`,
|
||||
}: {
|
||||
"leading": [
|
||||
SourceComment {
|
||||
text: "# own line comment",
|
||||
position: OwnLine,
|
||||
formatted: false,
|
||||
},
|
||||
],
|
||||
"dangling": [],
|
||||
"trailing": [],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,6 +3,21 @@ source: crates/ruff_python_formatter/src/comments/mod.rs
|
||||
expression: comments.debug(test_case.source_code)
|
||||
---
|
||||
{
|
||||
Node {
|
||||
kind: StmtMatch,
|
||||
range: 27..550,
|
||||
source: `match pt:⏎`,
|
||||
}: {
|
||||
"leading": [],
|
||||
"dangling": [],
|
||||
"trailing": [
|
||||
SourceComment {
|
||||
text: "# After match comment",
|
||||
position: OwnLine,
|
||||
formatted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
Node {
|
||||
kind: MatchCase,
|
||||
range: 84..132,
|
||||
@@ -108,19 +123,4 @@ expression: comments.debug(test_case.source_code)
|
||||
},
|
||||
],
|
||||
},
|
||||
Node {
|
||||
kind: StmtExpr,
|
||||
range: 656..670,
|
||||
source: `print("other")`,
|
||||
}: {
|
||||
"leading": [
|
||||
SourceComment {
|
||||
text: "# After match comment",
|
||||
position: OwnLine,
|
||||
formatted: false,
|
||||
},
|
||||
],
|
||||
"dangling": [],
|
||||
"trailing": [],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ use ruff_python_ast::{Decorator, StmtClassDef};
|
||||
use ruff_python_trivia::lines_after_ignoring_trivia;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::comments::format::empty_lines_before_trailing_comments;
|
||||
use crate::comments::{leading_comments, trailing_comments, SourceComment};
|
||||
use crate::context::NodeLevel;
|
||||
use crate::prelude::*;
|
||||
use crate::statement::clause::{clause_body, clause_header, ClauseHeader};
|
||||
use crate::statement::suite::SuiteKind;
|
||||
@@ -108,7 +110,33 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
|
||||
),
|
||||
clause_body(body, trailing_definition_comments).with_kind(SuiteKind::Class),
|
||||
]
|
||||
)?;
|
||||
|
||||
// If the class contains trailing comments, insert newlines before them.
|
||||
// For example, given:
|
||||
// ```python
|
||||
// class Class:
|
||||
// ...
|
||||
// # comment
|
||||
// ```
|
||||
//
|
||||
// At the top-level, reformat as:
|
||||
// ```python
|
||||
// class Class:
|
||||
// ...
|
||||
//
|
||||
//
|
||||
// # comment
|
||||
// ```
|
||||
empty_lines_before_trailing_comments(
|
||||
comments.trailing(item),
|
||||
if f.context().node_level() == NodeLevel::TopLevel {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
},
|
||||
)
|
||||
.fmt(f)
|
||||
}
|
||||
|
||||
fn fmt_dangling_comments(
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::comments::format::empty_lines_before_trailing_comments;
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::{Parameters, StmtFunctionDef};
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::context::NodeLevel;
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
use crate::expression::parentheses::{Parentheses, Parenthesize};
|
||||
use crate::prelude::*;
|
||||
@@ -144,7 +146,33 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
|
||||
),
|
||||
clause_body(body, trailing_definition_comments).with_kind(SuiteKind::Function),
|
||||
]
|
||||
)?;
|
||||
|
||||
// If the function contains trailing comments, insert newlines before them.
|
||||
// For example, given:
|
||||
// ```python
|
||||
// def func():
|
||||
// ...
|
||||
// # comment
|
||||
// ```
|
||||
//
|
||||
// At the top-level, reformat as:
|
||||
// ```python
|
||||
// def func():
|
||||
// ...
|
||||
//
|
||||
//
|
||||
// # comment
|
||||
// ```
|
||||
empty_lines_before_trailing_comments(
|
||||
comments.trailing(item),
|
||||
if f.context().node_level() == NodeLevel::TopLevel {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
},
|
||||
)
|
||||
.fmt(f)
|
||||
}
|
||||
|
||||
fn fmt_dangling_comments(
|
||||
|
||||
@@ -2,7 +2,7 @@ use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWi
|
||||
use ruff_python_ast::helpers::is_compound_statement;
|
||||
use ruff_python_ast::node::AnyNodeRef;
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, ExprConstant, Stmt, Suite};
|
||||
use ruff_python_trivia::{lines_after_ignoring_trivia, lines_before};
|
||||
use ruff_python_trivia::{lines_after, lines_after_ignoring_trivia, lines_before};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::comments::{leading_comments, trailing_comments, Comments};
|
||||
@@ -143,7 +143,11 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||
};
|
||||
|
||||
while let Some(following) = iter.next() {
|
||||
if is_class_or_function_definition(preceding)
|
||||
// Add empty lines before and after a function or class definition. If the preceding
|
||||
// node is a function or class, and contains trailing comments, then the statement
|
||||
// itself will add the requisite empty lines when formatting its comments.
|
||||
if (is_class_or_function_definition(preceding)
|
||||
&& !comments.has_trailing_own_line(preceding))
|
||||
|| is_class_or_function_definition(following)
|
||||
{
|
||||
match self.kind {
|
||||
@@ -191,9 +195,13 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||
empty_line().fmt(f)?;
|
||||
}
|
||||
}
|
||||
} else if is_import_definition(preceding) && !is_import_definition(following) {
|
||||
} else if is_import_definition(preceding)
|
||||
&& (!is_import_definition(following) || comments.has_leading(following))
|
||||
{
|
||||
// Enforce _at least_ one empty line after an import statement (but allow up to
|
||||
// two at the top-level).
|
||||
// two at the top-level). In this context, "after an import statement" means that
|
||||
// that the previous node is an import, and the following node is an import _or_ has
|
||||
// a leading comment.
|
||||
match self.kind {
|
||||
SuiteKind::TopLevel => {
|
||||
match lines_after_ignoring_trivia(preceding.end(), source) {
|
||||
@@ -274,16 +282,21 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||
// it then counts the lines between the statement and the trailing comment, which is
|
||||
// always 0. This is why it skips any trailing trivia (trivia that's on the same line)
|
||||
// and counts the lines after.
|
||||
lines_after_ignoring_trivia(offset, source)
|
||||
lines_after(offset, source)
|
||||
};
|
||||
|
||||
let end = comments
|
||||
.trailing(preceding)
|
||||
.last()
|
||||
.map_or(preceding.end(), |comment| comment.slice().end());
|
||||
|
||||
match node_level {
|
||||
NodeLevel::TopLevel => match count_lines(preceding.end()) {
|
||||
NodeLevel::TopLevel => match count_lines(end) {
|
||||
0 | 1 => hard_line_break().fmt(f)?,
|
||||
2 => empty_line().fmt(f)?,
|
||||
_ => write!(f, [empty_line(), empty_line()])?,
|
||||
},
|
||||
NodeLevel::CompoundStatement => match count_lines(preceding.end()) {
|
||||
NodeLevel::CompoundStatement => match count_lines(end) {
|
||||
0 | 1 => hard_line_break().fmt(f)?,
|
||||
_ => empty_line().fmt(f)?,
|
||||
},
|
||||
|
||||
@@ -162,7 +162,7 @@ def f():
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,29 +1,182 @@
|
||||
@@ -1,29 +1,205 @@
|
||||
+# This file doesn't use the standard decomposition.
|
||||
+# Decorator syntax test cases are separated by double # comments.
|
||||
+# Those before the 'output' comment are valid under the old syntax.
|
||||
@@ -179,6 +179,7 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@decorator()
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -186,6 +187,7 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@decorator(arg)
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -193,6 +195,7 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@decorator(kwarg=0)
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -200,49 +203,50 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@decorator(*args)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
##
|
||||
|
||||
-@decorator()()
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@decorator(**kwargs)
|
||||
def f():
|
||||
...
|
||||
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
##
|
||||
|
||||
-@(decorator)
|
||||
+@decorator(*args, **kwargs)
|
||||
def f():
|
||||
...
|
||||
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
##
|
||||
|
||||
-@sequence["decorator"]
|
||||
+@decorator(
|
||||
+ *args,
|
||||
+ **kwargs,
|
||||
+)
|
||||
def f():
|
||||
...
|
||||
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
##
|
||||
|
||||
-@decorator[List[str]]
|
||||
+@dotted.decorator
|
||||
def f():
|
||||
...
|
||||
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
##
|
||||
|
||||
-@var := decorator
|
||||
+@dotted.decorator(arg)
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -250,43 +254,54 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@dotted.decorator(kwarg=0)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
##
|
||||
|
||||
-@decorator()()
|
||||
+
|
||||
+@dotted.decorator(*args)
|
||||
+def f():
|
||||
+ ...
|
||||
def f():
|
||||
...
|
||||
|
||||
+
|
||||
+
|
||||
+##
|
||||
##
|
||||
|
||||
-@(decorator)
|
||||
+
|
||||
+@dotted.decorator(**kwargs)
|
||||
+def f():
|
||||
+ ...
|
||||
def f():
|
||||
...
|
||||
|
||||
+
|
||||
+
|
||||
+##
|
||||
##
|
||||
|
||||
-@sequence["decorator"]
|
||||
+
|
||||
+@dotted.decorator(*args, **kwargs)
|
||||
+def f():
|
||||
+ ...
|
||||
def f():
|
||||
...
|
||||
|
||||
+
|
||||
+
|
||||
+##
|
||||
##
|
||||
|
||||
-@decorator[List[str]]
|
||||
+
|
||||
+@dotted.decorator(
|
||||
+ *args,
|
||||
+ **kwargs,
|
||||
+)
|
||||
+def f():
|
||||
+ ...
|
||||
def f():
|
||||
...
|
||||
|
||||
+
|
||||
+
|
||||
+##
|
||||
##
|
||||
|
||||
-@var := decorator
|
||||
+
|
||||
+@double.dotted.decorator
|
||||
+def f():
|
||||
@@ -295,6 +310,7 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@double.dotted.decorator(arg)
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -302,6 +318,7 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@double.dotted.decorator(kwarg=0)
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -309,6 +326,7 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@double.dotted.decorator(*args)
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -316,6 +334,7 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@double.dotted.decorator(**kwargs)
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -323,6 +342,7 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@double.dotted.decorator(*args, **kwargs)
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -330,6 +350,7 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@double.dotted.decorator(
|
||||
+ *args,
|
||||
+ **kwargs,
|
||||
@@ -340,6 +361,7 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@_(sequence["decorator"])
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -347,6 +369,7 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@eval("sequence['decorator']")
|
||||
def f():
|
||||
...
|
||||
@@ -371,6 +394,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@decorator()
|
||||
def f():
|
||||
...
|
||||
@@ -378,6 +402,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@decorator(arg)
|
||||
def f():
|
||||
...
|
||||
@@ -385,6 +410,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@decorator(kwarg=0)
|
||||
def f():
|
||||
...
|
||||
@@ -392,6 +418,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@decorator(*args)
|
||||
def f():
|
||||
...
|
||||
@@ -399,6 +426,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@decorator(**kwargs)
|
||||
def f():
|
||||
...
|
||||
@@ -406,6 +434,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@decorator(*args, **kwargs)
|
||||
def f():
|
||||
...
|
||||
@@ -413,6 +442,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@decorator(
|
||||
*args,
|
||||
**kwargs,
|
||||
@@ -423,6 +453,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@dotted.decorator
|
||||
def f():
|
||||
...
|
||||
@@ -430,6 +461,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@dotted.decorator(arg)
|
||||
def f():
|
||||
...
|
||||
@@ -437,6 +469,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@dotted.decorator(kwarg=0)
|
||||
def f():
|
||||
...
|
||||
@@ -444,6 +477,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@dotted.decorator(*args)
|
||||
def f():
|
||||
...
|
||||
@@ -451,6 +485,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@dotted.decorator(**kwargs)
|
||||
def f():
|
||||
...
|
||||
@@ -458,6 +493,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@dotted.decorator(*args, **kwargs)
|
||||
def f():
|
||||
...
|
||||
@@ -465,6 +501,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@dotted.decorator(
|
||||
*args,
|
||||
**kwargs,
|
||||
@@ -475,6 +512,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@double.dotted.decorator
|
||||
def f():
|
||||
...
|
||||
@@ -482,6 +520,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@double.dotted.decorator(arg)
|
||||
def f():
|
||||
...
|
||||
@@ -489,6 +528,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@double.dotted.decorator(kwarg=0)
|
||||
def f():
|
||||
...
|
||||
@@ -496,6 +536,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@double.dotted.decorator(*args)
|
||||
def f():
|
||||
...
|
||||
@@ -503,6 +544,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@double.dotted.decorator(**kwargs)
|
||||
def f():
|
||||
...
|
||||
@@ -510,6 +552,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@double.dotted.decorator(*args, **kwargs)
|
||||
def f():
|
||||
...
|
||||
@@ -517,6 +560,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@double.dotted.decorator(
|
||||
*args,
|
||||
**kwargs,
|
||||
@@ -527,6 +571,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@_(sequence["decorator"])
|
||||
def f():
|
||||
...
|
||||
@@ -534,6 +579,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@eval("sequence['decorator']")
|
||||
def f():
|
||||
...
|
||||
|
||||
@@ -156,7 +156,7 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite
|
||||
)
|
||||
|
||||
|
||||
@@ -108,11 +112,20 @@
|
||||
@@ -108,11 +112,18 @@
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
)
|
||||
|
||||
@@ -176,10 +176,7 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite
|
||||
+ ], # type: ignore
|
||||
)
|
||||
|
||||
-aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type]
|
||||
+aaaaaaaaaaaaa, bbbbbbbbb = map(
|
||||
+ list, map(itertools.chain.from_iterable, zip(*items))
|
||||
+) # type: ignore[arg-type]
|
||||
aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type]
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
@@ -313,9 +310,7 @@ call_to_some_function_asdf(
|
||||
], # type: ignore
|
||||
)
|
||||
|
||||
aaaaaaaaaaaaa, bbbbbbbbb = map(
|
||||
list, map(itertools.chain.from_iterable, zip(*items))
|
||||
) # type: ignore[arg-type]
|
||||
aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type]
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/empty_lines.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```py
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
# leading comment
|
||||
def f():
|
||||
NO = ''
|
||||
SPACE = ' '
|
||||
DOUBLESPACE = ' '
|
||||
|
||||
t = leaf.type
|
||||
p = leaf.parent # trailing comment
|
||||
v = leaf.value
|
||||
|
||||
if t in ALWAYS_NO_SPACE:
|
||||
pass
|
||||
if t == token.COMMENT: # another trailing comment
|
||||
return DOUBLESPACE
|
||||
|
||||
|
||||
assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}"
|
||||
|
||||
|
||||
prev = leaf.prev_sibling
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
if not prevp or prevp.type in OPENING_BRACKETS:
|
||||
|
||||
|
||||
return NO
|
||||
|
||||
|
||||
if prevp.type == token.EQUAL:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.argument,
|
||||
}:
|
||||
return NO
|
||||
|
||||
elif prevp.type == token.DOUBLESTAR:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.dictsetmaker,
|
||||
}:
|
||||
return NO
|
||||
|
||||
###############################################################################
|
||||
# SECTION BECAUSE SECTIONS
|
||||
###############################################################################
|
||||
|
||||
def g():
|
||||
NO = ''
|
||||
SPACE = ' '
|
||||
DOUBLESPACE = ' '
|
||||
|
||||
t = leaf.type
|
||||
p = leaf.parent
|
||||
v = leaf.value
|
||||
|
||||
# Comment because comments
|
||||
|
||||
if t in ALWAYS_NO_SPACE:
|
||||
pass
|
||||
if t == token.COMMENT:
|
||||
return DOUBLESPACE
|
||||
|
||||
# Another comment because more comments
|
||||
assert p is not None, f'INTERNAL ERROR: hand-made leaf without parent: {leaf!r}'
|
||||
|
||||
prev = leaf.prev_sibling
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
|
||||
if not prevp or prevp.type in OPENING_BRACKETS:
|
||||
# Start of the line or a bracketed expression.
|
||||
# More than one line for the comment.
|
||||
return NO
|
||||
|
||||
if prevp.type == token.EQUAL:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.argument,
|
||||
}:
|
||||
return NO
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -49,7 +49,6 @@
|
||||
# SECTION BECAUSE SECTIONS
|
||||
###############################################################################
|
||||
|
||||
-
|
||||
def g():
|
||||
NO = ""
|
||||
SPACE = " "
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```py
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
# leading comment
|
||||
def f():
|
||||
NO = ""
|
||||
SPACE = " "
|
||||
DOUBLESPACE = " "
|
||||
|
||||
t = leaf.type
|
||||
p = leaf.parent # trailing comment
|
||||
v = leaf.value
|
||||
|
||||
if t in ALWAYS_NO_SPACE:
|
||||
pass
|
||||
if t == token.COMMENT: # another trailing comment
|
||||
return DOUBLESPACE
|
||||
|
||||
assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}"
|
||||
|
||||
prev = leaf.prev_sibling
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
if not prevp or prevp.type in OPENING_BRACKETS:
|
||||
return NO
|
||||
|
||||
if prevp.type == token.EQUAL:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.argument,
|
||||
}:
|
||||
return NO
|
||||
|
||||
elif prevp.type == token.DOUBLESTAR:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.dictsetmaker,
|
||||
}:
|
||||
return NO
|
||||
|
||||
|
||||
###############################################################################
|
||||
# SECTION BECAUSE SECTIONS
|
||||
###############################################################################
|
||||
|
||||
def g():
|
||||
NO = ""
|
||||
SPACE = " "
|
||||
DOUBLESPACE = " "
|
||||
|
||||
t = leaf.type
|
||||
p = leaf.parent
|
||||
v = leaf.value
|
||||
|
||||
# Comment because comments
|
||||
|
||||
if t in ALWAYS_NO_SPACE:
|
||||
pass
|
||||
if t == token.COMMENT:
|
||||
return DOUBLESPACE
|
||||
|
||||
# Another comment because more comments
|
||||
assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}"
|
||||
|
||||
prev = leaf.prev_sibling
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
|
||||
if not prevp or prevp.type in OPENING_BRACKETS:
|
||||
# Start of the line or a bracketed expression.
|
||||
# More than one line for the comment.
|
||||
return NO
|
||||
|
||||
if prevp.type == token.EQUAL:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.argument,
|
||||
}:
|
||||
return NO
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```py
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
# leading comment
|
||||
def f():
|
||||
NO = ""
|
||||
SPACE = " "
|
||||
DOUBLESPACE = " "
|
||||
|
||||
t = leaf.type
|
||||
p = leaf.parent # trailing comment
|
||||
v = leaf.value
|
||||
|
||||
if t in ALWAYS_NO_SPACE:
|
||||
pass
|
||||
if t == token.COMMENT: # another trailing comment
|
||||
return DOUBLESPACE
|
||||
|
||||
assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}"
|
||||
|
||||
prev = leaf.prev_sibling
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
if not prevp or prevp.type in OPENING_BRACKETS:
|
||||
return NO
|
||||
|
||||
if prevp.type == token.EQUAL:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.argument,
|
||||
}:
|
||||
return NO
|
||||
|
||||
elif prevp.type == token.DOUBLESTAR:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.dictsetmaker,
|
||||
}:
|
||||
return NO
|
||||
|
||||
|
||||
###############################################################################
|
||||
# SECTION BECAUSE SECTIONS
|
||||
###############################################################################
|
||||
|
||||
|
||||
def g():
|
||||
NO = ""
|
||||
SPACE = " "
|
||||
DOUBLESPACE = " "
|
||||
|
||||
t = leaf.type
|
||||
p = leaf.parent
|
||||
v = leaf.value
|
||||
|
||||
# Comment because comments
|
||||
|
||||
if t in ALWAYS_NO_SPACE:
|
||||
pass
|
||||
if t == token.COMMENT:
|
||||
return DOUBLESPACE
|
||||
|
||||
# Another comment because more comments
|
||||
assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}"
|
||||
|
||||
prev = leaf.prev_sibling
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
|
||||
if not prevp or prevp.type in OPENING_BRACKETS:
|
||||
# Start of the line or a bracketed expression.
|
||||
# More than one line for the comment.
|
||||
return NO
|
||||
|
||||
if prevp.type == token.EQUAL:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.argument,
|
||||
}:
|
||||
return NO
|
||||
```
|
||||
|
||||
|
||||
@@ -300,17 +300,6 @@ last_call()
|
||||
) # note: no trailing comma pre-3.6
|
||||
call(*gidgets[:2])
|
||||
call(a, *gidgets[:2])
|
||||
@@ -142,7 +143,9 @@
|
||||
xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore
|
||||
sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)
|
||||
)
|
||||
-xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore
|
||||
+xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[
|
||||
+ ..., List[SomeClass]
|
||||
+] = classmethod( # type: ignore
|
||||
sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)
|
||||
)
|
||||
xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod(
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
@@ -461,9 +450,7 @@ very_long_variable_name_filters: t.List[
|
||||
xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore
|
||||
sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)
|
||||
)
|
||||
xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[
|
||||
..., List[SomeClass]
|
||||
] = classmethod( # type: ignore
|
||||
xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore
|
||||
sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)
|
||||
)
|
||||
xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod(
|
||||
|
||||
@@ -198,7 +198,15 @@ d={'a':1,
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -63,15 +63,15 @@
|
||||
@@ -5,6 +5,7 @@
|
||||
from third_party import X, Y, Z
|
||||
|
||||
from library import some_connection, some_decorator
|
||||
+
|
||||
# fmt: off
|
||||
from third_party import (X,
|
||||
Y, Z)
|
||||
@@ -63,15 +64,15 @@
|
||||
|
||||
something = {
|
||||
# fmt: off
|
||||
@@ -217,7 +225,7 @@ d={'a':1,
|
||||
# fmt: on
|
||||
goes + here,
|
||||
andhere,
|
||||
@@ -122,8 +122,10 @@
|
||||
@@ -122,8 +123,10 @@
|
||||
"""
|
||||
# fmt: off
|
||||
|
||||
@@ -229,7 +237,7 @@ d={'a':1,
|
||||
# fmt: on
|
||||
pass
|
||||
|
||||
@@ -138,7 +140,7 @@
|
||||
@@ -138,7 +141,7 @@
|
||||
now . considers . multiple . fmt . directives . within . one . prefix
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
@@ -238,7 +246,7 @@ d={'a':1,
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -178,14 +180,18 @@
|
||||
@@ -178,14 +181,18 @@
|
||||
$
|
||||
""",
|
||||
# fmt: off
|
||||
@@ -271,6 +279,7 @@ import sys
|
||||
from third_party import X, Y, Z
|
||||
|
||||
from library import some_connection, some_decorator
|
||||
|
||||
# fmt: off
|
||||
from third_party import (X,
|
||||
Y, Z)
|
||||
|
||||
@@ -110,15 +110,7 @@ elif unformatted:
|
||||
},
|
||||
)
|
||||
|
||||
@@ -74,7 +73,6 @@
|
||||
class Factory(t.Protocol):
|
||||
def this_will_be_formatted(self, **kwargs) -> Named:
|
||||
...
|
||||
-
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -82,6 +80,6 @@
|
||||
@@ -82,6 +81,6 @@
|
||||
if x:
|
||||
return x
|
||||
# fmt: off
|
||||
@@ -206,6 +198,7 @@ class Named(t.Protocol):
|
||||
class Factory(t.Protocol):
|
||||
def this_will_be_formatted(self, **kwargs) -> Named:
|
||||
...
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```py
|
||||
# Regression test for https://github.com/psf/black/issues/3438
|
||||
|
||||
import ast
|
||||
import collections # fmt: skip
|
||||
import dataclasses
|
||||
# fmt: off
|
||||
import os
|
||||
# fmt: on
|
||||
import pathlib
|
||||
|
||||
import re # fmt: skip
|
||||
import secrets
|
||||
|
||||
# fmt: off
|
||||
import sys
|
||||
# fmt: on
|
||||
|
||||
import tempfile
|
||||
import zoneinfo
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -3,6 +3,7 @@
|
||||
import ast
|
||||
import collections # fmt: skip
|
||||
import dataclasses
|
||||
+
|
||||
# fmt: off
|
||||
import os
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```py
|
||||
# Regression test for https://github.com/psf/black/issues/3438
|
||||
|
||||
import ast
|
||||
import collections # fmt: skip
|
||||
import dataclasses
|
||||
|
||||
# fmt: off
|
||||
import os
|
||||
# fmt: on
|
||||
import pathlib
|
||||
|
||||
import re # fmt: skip
|
||||
import secrets
|
||||
|
||||
# fmt: off
|
||||
import sys
|
||||
# fmt: on
|
||||
|
||||
import tempfile
|
||||
import zoneinfo
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```py
|
||||
# Regression test for https://github.com/psf/black/issues/3438
|
||||
|
||||
import ast
|
||||
import collections # fmt: skip
|
||||
import dataclasses
|
||||
# fmt: off
|
||||
import os
|
||||
# fmt: on
|
||||
import pathlib
|
||||
|
||||
import re # fmt: skip
|
||||
import secrets
|
||||
|
||||
# fmt: off
|
||||
import sys
|
||||
# fmt: on
|
||||
|
||||
import tempfile
|
||||
import zoneinfo
|
||||
```
|
||||
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/power_op_spacing.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```py
|
||||
def function(**kwargs):
|
||||
t = a**2 + b**3
|
||||
return t ** 2
|
||||
|
||||
|
||||
def function_replace_spaces(**kwargs):
|
||||
t = a **2 + b** 3 + c ** 4
|
||||
|
||||
|
||||
def function_dont_replace_spaces():
|
||||
{**a, **b, **c}
|
||||
|
||||
|
||||
a = 5**~4
|
||||
b = 5 ** f()
|
||||
c = -(5**2)
|
||||
d = 5 ** f["hi"]
|
||||
e = lazy(lambda **kwargs: 5)
|
||||
f = f() ** 5
|
||||
g = a.b**c.d
|
||||
h = 5 ** funcs.f()
|
||||
i = funcs.f() ** 5
|
||||
j = super().name ** 5
|
||||
k = [(2**idx, value) for idx, value in pairs]
|
||||
l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
|
||||
m = [([2**63], [1, 2**63])]
|
||||
n = count <= 10**5
|
||||
o = settings(max_examples=10**6)
|
||||
p = {(k, k**2): v**2 for k, v in pairs}
|
||||
q = [10**i for i in range(6)]
|
||||
r = x**y
|
||||
|
||||
a = 5.0**~4.0
|
||||
b = 5.0 ** f()
|
||||
c = -(5.0**2.0)
|
||||
d = 5.0 ** f["hi"]
|
||||
e = lazy(lambda **kwargs: 5)
|
||||
f = f() ** 5.0
|
||||
g = a.b**c.d
|
||||
h = 5.0 ** funcs.f()
|
||||
i = funcs.f() ** 5.0
|
||||
j = super().name ** 5.0
|
||||
k = [(2.0**idx, value) for idx, value in pairs]
|
||||
l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
|
||||
m = [([2.0**63.0], [1.0, 2**63.0])]
|
||||
n = count <= 10**5.0
|
||||
o = settings(max_examples=10**6.0)
|
||||
p = {(k, k**2): v**2.0 for k, v in pairs}
|
||||
q = [10.5**i for i in range(6)]
|
||||
|
||||
|
||||
# WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873)
|
||||
if hasattr(view, "sum_of_weights"):
|
||||
return np.divide( # type: ignore[no-any-return]
|
||||
view.variance, # type: ignore[union-attr]
|
||||
view.sum_of_weights, # type: ignore[union-attr]
|
||||
out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr]
|
||||
where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr]
|
||||
)
|
||||
|
||||
return np.divide(
|
||||
where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore
|
||||
)
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -55,9 +55,11 @@
|
||||
view.variance, # type: ignore[union-attr]
|
||||
view.sum_of_weights, # type: ignore[union-attr]
|
||||
out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr]
|
||||
- where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr]
|
||||
+ where=view.sum_of_weights**2
|
||||
+ > view.sum_of_weights_squared, # type: ignore[union-attr]
|
||||
)
|
||||
|
||||
return np.divide(
|
||||
- where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore
|
||||
+ where=view.sum_of_weights_of_weight_long**2
|
||||
+ > view.sum_of_weights_squared, # type: ignore
|
||||
)
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```py
|
||||
def function(**kwargs):
|
||||
t = a**2 + b**3
|
||||
return t**2
|
||||
|
||||
|
||||
def function_replace_spaces(**kwargs):
|
||||
t = a**2 + b**3 + c**4
|
||||
|
||||
|
||||
def function_dont_replace_spaces():
|
||||
{**a, **b, **c}
|
||||
|
||||
|
||||
a = 5**~4
|
||||
b = 5 ** f()
|
||||
c = -(5**2)
|
||||
d = 5 ** f["hi"]
|
||||
e = lazy(lambda **kwargs: 5)
|
||||
f = f() ** 5
|
||||
g = a.b**c.d
|
||||
h = 5 ** funcs.f()
|
||||
i = funcs.f() ** 5
|
||||
j = super().name ** 5
|
||||
k = [(2**idx, value) for idx, value in pairs]
|
||||
l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
|
||||
m = [([2**63], [1, 2**63])]
|
||||
n = count <= 10**5
|
||||
o = settings(max_examples=10**6)
|
||||
p = {(k, k**2): v**2 for k, v in pairs}
|
||||
q = [10**i for i in range(6)]
|
||||
r = x**y
|
||||
|
||||
a = 5.0**~4.0
|
||||
b = 5.0 ** f()
|
||||
c = -(5.0**2.0)
|
||||
d = 5.0 ** f["hi"]
|
||||
e = lazy(lambda **kwargs: 5)
|
||||
f = f() ** 5.0
|
||||
g = a.b**c.d
|
||||
h = 5.0 ** funcs.f()
|
||||
i = funcs.f() ** 5.0
|
||||
j = super().name ** 5.0
|
||||
k = [(2.0**idx, value) for idx, value in pairs]
|
||||
l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
|
||||
m = [([2.0**63.0], [1.0, 2**63.0])]
|
||||
n = count <= 10**5.0
|
||||
o = settings(max_examples=10**6.0)
|
||||
p = {(k, k**2): v**2.0 for k, v in pairs}
|
||||
q = [10.5**i for i in range(6)]
|
||||
|
||||
|
||||
# WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873)
|
||||
if hasattr(view, "sum_of_weights"):
|
||||
return np.divide( # type: ignore[no-any-return]
|
||||
view.variance, # type: ignore[union-attr]
|
||||
view.sum_of_weights, # type: ignore[union-attr]
|
||||
out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr]
|
||||
where=view.sum_of_weights**2
|
||||
> view.sum_of_weights_squared, # type: ignore[union-attr]
|
||||
)
|
||||
|
||||
return np.divide(
|
||||
where=view.sum_of_weights_of_weight_long**2
|
||||
> view.sum_of_weights_squared, # type: ignore
|
||||
)
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```py
|
||||
def function(**kwargs):
|
||||
t = a**2 + b**3
|
||||
return t**2
|
||||
|
||||
|
||||
def function_replace_spaces(**kwargs):
|
||||
t = a**2 + b**3 + c**4
|
||||
|
||||
|
||||
def function_dont_replace_spaces():
|
||||
{**a, **b, **c}
|
||||
|
||||
|
||||
a = 5**~4
|
||||
b = 5 ** f()
|
||||
c = -(5**2)
|
||||
d = 5 ** f["hi"]
|
||||
e = lazy(lambda **kwargs: 5)
|
||||
f = f() ** 5
|
||||
g = a.b**c.d
|
||||
h = 5 ** funcs.f()
|
||||
i = funcs.f() ** 5
|
||||
j = super().name ** 5
|
||||
k = [(2**idx, value) for idx, value in pairs]
|
||||
l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
|
||||
m = [([2**63], [1, 2**63])]
|
||||
n = count <= 10**5
|
||||
o = settings(max_examples=10**6)
|
||||
p = {(k, k**2): v**2 for k, v in pairs}
|
||||
q = [10**i for i in range(6)]
|
||||
r = x**y
|
||||
|
||||
a = 5.0**~4.0
|
||||
b = 5.0 ** f()
|
||||
c = -(5.0**2.0)
|
||||
d = 5.0 ** f["hi"]
|
||||
e = lazy(lambda **kwargs: 5)
|
||||
f = f() ** 5.0
|
||||
g = a.b**c.d
|
||||
h = 5.0 ** funcs.f()
|
||||
i = funcs.f() ** 5.0
|
||||
j = super().name ** 5.0
|
||||
k = [(2.0**idx, value) for idx, value in pairs]
|
||||
l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
|
||||
m = [([2.0**63.0], [1.0, 2**63.0])]
|
||||
n = count <= 10**5.0
|
||||
o = settings(max_examples=10**6.0)
|
||||
p = {(k, k**2): v**2.0 for k, v in pairs}
|
||||
q = [10.5**i for i in range(6)]
|
||||
|
||||
|
||||
# WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873)
|
||||
if hasattr(view, "sum_of_weights"):
|
||||
return np.divide( # type: ignore[no-any-return]
|
||||
view.variance, # type: ignore[union-attr]
|
||||
view.sum_of_weights, # type: ignore[union-attr]
|
||||
out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr]
|
||||
where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr]
|
||||
)
|
||||
|
||||
return np.divide(
|
||||
where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -50,17 +50,14 @@ assert (
|
||||
) #
|
||||
|
||||
assert sort_by_dependency(
|
||||
@@ -25,9 +25,11 @@
|
||||
@@ -25,9 +25,9 @@
|
||||
class A:
|
||||
def foo(self):
|
||||
for _ in range(10):
|
||||
- aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc(
|
||||
- xxxxxxxxxxxx
|
||||
+ aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc( # pylint: disable=no-member
|
||||
xxxxxxxxxxxx
|
||||
- ) # pylint: disable=no-member
|
||||
+ aaaaaaaaaaaaaaaaaaa = (
|
||||
+ bbbbbbbbbbbbbbb.cccccccccc( # pylint: disable=no-member
|
||||
+ xxxxxxxxxxxx
|
||||
+ )
|
||||
+ )
|
||||
|
||||
|
||||
@@ -97,10 +94,8 @@ importA
|
||||
class A:
|
||||
def foo(self):
|
||||
for _ in range(10):
|
||||
aaaaaaaaaaaaaaaaaaa = (
|
||||
bbbbbbbbbbbbbbb.cccccccccc( # pylint: disable=no-member
|
||||
xxxxxxxxxxxx
|
||||
)
|
||||
aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc( # pylint: disable=no-member
|
||||
xxxxxxxxxxxx
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,30 @@ result_f = (
|
||||
# comment
|
||||
''
|
||||
)
|
||||
|
||||
(
|
||||
f'{1}' # comment
|
||||
f'{2}'
|
||||
)
|
||||
|
||||
(
|
||||
f'{1}'
|
||||
f'{2}' # comment
|
||||
)
|
||||
|
||||
(
|
||||
1, ( # comment
|
||||
f'{2}'
|
||||
)
|
||||
)
|
||||
|
||||
(
|
||||
(
|
||||
f'{1}'
|
||||
# comment
|
||||
),
|
||||
2
|
||||
)
|
||||
```
|
||||
|
||||
## Output
|
||||
@@ -76,6 +100,30 @@ result_f = (
|
||||
# comment
|
||||
""
|
||||
)
|
||||
|
||||
(
|
||||
f"{1}" # comment
|
||||
f"{2}"
|
||||
)
|
||||
|
||||
(
|
||||
f"{1}" f"{2}" # comment
|
||||
)
|
||||
|
||||
(
|
||||
1,
|
||||
( # comment
|
||||
f"{2}"
|
||||
),
|
||||
)
|
||||
|
||||
(
|
||||
(
|
||||
f"{1}"
|
||||
# comment
|
||||
),
|
||||
2,
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -4,61 +4,6 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off
|
||||
---
|
||||
## Input
|
||||
```py
|
||||
def test():
|
||||
# fmt: off
|
||||
a_very_small_indent
|
||||
(
|
||||
not_fixed
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
more
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
def test():
|
||||
a_small_indent
|
||||
# fmt: off
|
||||
# fix under-indented comments
|
||||
(or_the_inner_expression +
|
||||
expressions
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def test():
|
||||
pass
|
||||
|
||||
# It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment
|
||||
# of the `test` function if the "proper" indentation is larger than 2 spaces.
|
||||
# fmt: on
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted;
|
||||
|
||||
def test():
|
||||
pass
|
||||
# fmt: off
|
||||
"""A multiline strings
|
||||
that should not get formatted"""
|
||||
|
||||
"A single quoted multiline \
|
||||
string"
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted;
|
||||
```
|
||||
|
||||
## Outputs
|
||||
@@ -72,63 +17,6 @@ magic-trailing-comma = Respect
|
||||
```
|
||||
|
||||
```py
|
||||
def test():
|
||||
# fmt: off
|
||||
a_very_small_indent
|
||||
(
|
||||
not_fixed
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
more
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
def test():
|
||||
a_small_indent
|
||||
# fmt: off
|
||||
# fix under-indented comments
|
||||
(or_the_inner_expression +
|
||||
expressions
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def test():
|
||||
pass
|
||||
|
||||
# It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment
|
||||
# of the `test` function if the "proper" indentation is larger than 2 spaces.
|
||||
# fmt: on
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
|
||||
def test():
|
||||
pass
|
||||
# fmt: off
|
||||
"""A multiline strings
|
||||
that should not get formatted"""
|
||||
|
||||
"A single quoted multiline \
|
||||
string"
|
||||
|
||||
disabled + formatting
|
||||
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
```
|
||||
|
||||
|
||||
@@ -142,63 +30,6 @@ magic-trailing-comma = Respect
|
||||
```
|
||||
|
||||
```py
|
||||
def test():
|
||||
# fmt: off
|
||||
a_very_small_indent
|
||||
(
|
||||
not_fixed
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
more
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
def test():
|
||||
a_small_indent
|
||||
# fmt: off
|
||||
# fix under-indented comments
|
||||
(or_the_inner_expression +
|
||||
expressions
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def test():
|
||||
pass
|
||||
|
||||
# It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment
|
||||
# of the `test` function if the "proper" indentation is larger than 2 spaces.
|
||||
# fmt: on
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
|
||||
def test():
|
||||
pass
|
||||
# fmt: off
|
||||
"""A multiline strings
|
||||
that should not get formatted"""
|
||||
|
||||
"A single quoted multiline \
|
||||
string"
|
||||
|
||||
disabled + formatting
|
||||
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
```
|
||||
|
||||
|
||||
@@ -212,63 +43,6 @@ magic-trailing-comma = Respect
|
||||
```
|
||||
|
||||
```py
|
||||
def test():
|
||||
# fmt: off
|
||||
a_very_small_indent
|
||||
(
|
||||
not_fixed
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
more
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
def test():
|
||||
a_small_indent
|
||||
# fmt: off
|
||||
# fix under-indented comments
|
||||
(or_the_inner_expression +
|
||||
expressions
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def test():
|
||||
pass
|
||||
|
||||
# It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment
|
||||
# of the `test` function if the "proper" indentation is larger than 2 spaces.
|
||||
# fmt: on
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
|
||||
def test():
|
||||
pass
|
||||
# fmt: off
|
||||
"""A multiline strings
|
||||
that should not get formatted"""
|
||||
|
||||
"A single quoted multiline \
|
||||
string"
|
||||
|
||||
disabled + formatting
|
||||
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ not_fixed
|
||||
more
|
||||
else:
|
||||
other
|
||||
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
@@ -72,6 +74,8 @@ not_fixed
|
||||
more
|
||||
else:
|
||||
other
|
||||
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
@@ -99,6 +103,8 @@ not_fixed
|
||||
more
|
||||
else:
|
||||
other
|
||||
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/newlines.py
|
||||
---
|
||||
## Input
|
||||
```py
|
||||
def func():
|
||||
pass
|
||||
# fmt: off
|
||||
x = 1
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
# fmt: on
|
||||
x = 1
|
||||
|
||||
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
# fmt: on
|
||||
def func():
|
||||
pass
|
||||
|
||||
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
|
||||
|
||||
# fmt: on
|
||||
def func():
|
||||
pass
|
||||
# fmt: on
|
||||
def func():
|
||||
pass
|
||||
```
|
||||
|
||||
## Output
|
||||
```py
|
||||
def func():
|
||||
pass
|
||||
|
||||
|
||||
# fmt: off
|
||||
x = 1
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
# fmt: on
|
||||
x = 1
|
||||
|
||||
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
# fmt: on
|
||||
def func():
|
||||
pass
|
||||
|
||||
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
|
||||
|
||||
# fmt: on
|
||||
def func():
|
||||
pass
|
||||
|
||||
|
||||
# fmt: on
|
||||
def func():
|
||||
pass
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/newlines.py
|
||||
---
|
||||
## Input
|
||||
```py
|
||||
###
|
||||
# Blank lines around functions
|
||||
###
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
if True:
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
|
||||
# comment
|
||||
def f():
|
||||
pass
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
# comment
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
# comment
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
###
|
||||
# Blank lines around imports.
|
||||
###
|
||||
|
||||
def f():
|
||||
import x
|
||||
# comment
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
# comment
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
# comment
|
||||
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
|
||||
# comment
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x # comment
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f(): pass # comment
|
||||
# comment
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
x = 1
|
||||
```
|
||||
|
||||
## Output
|
||||
```py
|
||||
###
|
||||
# Blank lines around functions
|
||||
###
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
if True:
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
# comment
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
# comment
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
# comment
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
# comment
|
||||
# comment
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
###
|
||||
# Blank lines around imports.
|
||||
###
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
# comment
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
# comment
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
# comment
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x # comment
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
pass # comment
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
x = 1
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -191,10 +191,9 @@ assert (
|
||||
# Trailing test value own-line
|
||||
# Test dangler
|
||||
), "Some string" # Trailing msg same-line
|
||||
|
||||
|
||||
# Trailing assert
|
||||
|
||||
|
||||
def test():
|
||||
assert (
|
||||
{
|
||||
|
||||
@@ -406,6 +406,7 @@ def test(
|
||||
|
||||
### Different function argument wrappings
|
||||
|
||||
|
||||
def single_line(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccccccccc):
|
||||
pass
|
||||
|
||||
@@ -511,6 +512,7 @@ def type_param_comments[ # trailing bracket comment
|
||||
|
||||
# Different type parameter wrappings
|
||||
|
||||
|
||||
def single_line[Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, Bbbbbbbbbbbbbbb, Ccccccccccccccccc]():
|
||||
pass
|
||||
|
||||
|
||||
@@ -4,22 +4,72 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/trailing_c
|
||||
---
|
||||
## Input
|
||||
```py
|
||||
# Pragma reserved width fixtures
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # nocoverage: This should break
|
||||
|
||||
|
||||
# Pragma fixtures for non-breaking space (lead by NBSP)
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # nocoverage: This should break
|
||||
|
||||
|
||||
# As of adding this fixture Black adds a space before the non-breaking space if part of a type pragma.
|
||||
# https://github.com/psf/black/blob/b4dca26c7d93f930bbd5a7b552807370b60d4298/src/black/comments.py#L122-L129
|
||||
i2 = "" # type: Add space before leading NBSP followed by spaces
|
||||
i3 = "" #type: A space is added
|
||||
i4 = "" # type: Add space before leading NBSP followed by a space
|
||||
i5 = "" # type: Add space before leading NBSP
|
||||
i = "" # type: Add space before leading NBSP followed by spaces
|
||||
i = "" #type: A space is added
|
||||
i = "" # type: Add space before leading NBSP followed by a space
|
||||
i = "" # type: Add space before leading NBSP
|
||||
i = "" # type: Add space before two leading NBSP
|
||||
|
||||
|
||||
# A noqa as `#\u{A0}\u{A0}noqa` becomes `# \u{A0}noqa`
|
||||
i = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # noqa
|
||||
```
|
||||
|
||||
## Output
|
||||
```py
|
||||
# Pragma reserved width fixtures
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break
|
||||
i = (
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
) # nocoverage: This should break
|
||||
|
||||
|
||||
# Pragma fixtures for non-breaking space (lead by NBSP)
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break
|
||||
i = (
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
) # nocoverage: This should break
|
||||
|
||||
|
||||
# As of adding this fixture Black adds a space before the non-breaking space if part of a type pragma.
|
||||
# https://github.com/psf/black/blob/b4dca26c7d93f930bbd5a7b552807370b60d4298/src/black/comments.py#L122-L129
|
||||
i2 = "" # type: Add space before leading NBSP followed by spaces
|
||||
i3 = "" # type: A space is added
|
||||
i4 = "" # type: Add space before leading NBSP followed by a space
|
||||
i5 = "" # type: Add space before leading NBSP
|
||||
i = "" # type: Add space before leading NBSP followed by spaces
|
||||
i = "" # type: A space is added
|
||||
i = "" # type: Add space before leading NBSP followed by a space
|
||||
i = "" # type: Add space before leading NBSP
|
||||
i = "" # type: Add space before two leading NBSP
|
||||
|
||||
|
||||
# A noqa as `#\u{A0}\u{A0}noqa` becomes `# \u{A0}noqa`
|
||||
i = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # noqa
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -643,6 +643,30 @@ with (0 as a, 1 as b,): pass
|
||||
insta::assert_debug_snapshot!(parse_suite(source, "<test>").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parenthesized_with_statement() {
|
||||
let source = "\
|
||||
with ((a), (b)): pass
|
||||
with ((a), (b), c as d, (e)): pass
|
||||
with (a, b): pass
|
||||
with (a, b) as c: pass
|
||||
with ((a, b) as c): pass
|
||||
with (a as b): pass
|
||||
with (a): pass
|
||||
with (a := 0): pass
|
||||
with (a := 0) as x: pass
|
||||
with ((a)): pass
|
||||
with ((a := 0)): pass
|
||||
with (a as b, (a := 0)): pass
|
||||
with (a, (a := 0)): pass
|
||||
with (yield): pass
|
||||
with (yield from a): pass
|
||||
with ((yield)): pass
|
||||
with ((yield from a)): pass
|
||||
";
|
||||
insta::assert_debug_snapshot!(parse_suite(source, "<test>").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_statement_invalid() {
|
||||
for source in [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user