mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 06:42:02 +00:00
Add basic jupyter notebook support (#3440)
* Add basic jupyter notebook support behind a feature flag * Address review comments * Rename in separate commit to make both git and clippy happy * cfg(feature = "jupyter_notebook") another test * Address more review comments * Address more review comments * and clippy and windows * More review comment
This commit is contained in:
parent
a45753f462
commit
81d0884974
19 changed files with 1035 additions and 68 deletions
|
@ -19,6 +19,7 @@ use serde::Serialize;
|
|||
use serde_json::json;
|
||||
|
||||
use ruff::fs::{relativize_path, relativize_path_to};
|
||||
use ruff::jupyter::JupyterIndex;
|
||||
use ruff::linter::FixTable;
|
||||
use ruff::logging::LogLevel;
|
||||
use ruff::message::{Location, Message};
|
||||
|
@ -162,34 +163,32 @@ impl Printer {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_once(&self, diagnostics: &Diagnostics) -> Result<()> {
|
||||
pub fn write_once(&self, diagnostics: &Diagnostics, writer: &mut impl Write) -> Result<()> {
|
||||
if matches!(self.log_level, LogLevel::Silent) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !self.flags.contains(Flags::SHOW_VIOLATIONS) {
|
||||
let mut stdout = BufWriter::new(io::stdout().lock());
|
||||
if matches!(
|
||||
self.format,
|
||||
SerializationFormat::Text | SerializationFormat::Grouped
|
||||
) {
|
||||
if self.flags.contains(Flags::SHOW_FIXES) {
|
||||
if !diagnostics.fixed.is_empty() {
|
||||
writeln!(stdout)?;
|
||||
print_fixed(&mut stdout, &diagnostics.fixed)?;
|
||||
writeln!(stdout)?;
|
||||
writeln!(writer)?;
|
||||
print_fixed(writer, &diagnostics.fixed)?;
|
||||
writeln!(writer)?;
|
||||
}
|
||||
}
|
||||
self.post_text(&mut stdout, diagnostics)?;
|
||||
self.post_text(writer, diagnostics)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut stdout = BufWriter::new(io::stdout().lock());
|
||||
match self.format {
|
||||
SerializationFormat::Json => {
|
||||
writeln!(
|
||||
stdout,
|
||||
writer,
|
||||
"{}",
|
||||
serde_json::to_string_pretty(
|
||||
&diagnostics
|
||||
|
@ -225,12 +224,20 @@ impl Printer {
|
|||
for message in messages {
|
||||
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
|
||||
status.set_message(message.kind.body.clone());
|
||||
status.set_description(format!(
|
||||
"line {}, col {}, {}",
|
||||
message.location.row(),
|
||||
message.location.column(),
|
||||
message.kind.body
|
||||
));
|
||||
let description =
|
||||
if diagnostics.jupyter_index.contains_key(&message.filename) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
format!("line 1, col 0, {}", message.kind.body)
|
||||
} else {
|
||||
format!(
|
||||
"line {}, col {}, {}",
|
||||
message.location.row(),
|
||||
message.location.column(),
|
||||
message.kind.body
|
||||
)
|
||||
};
|
||||
status.set_description(description);
|
||||
let mut case = TestCase::new(
|
||||
format!("org.ruff.{}", message.kind.rule().noqa_code()),
|
||||
status,
|
||||
|
@ -248,20 +255,25 @@ impl Printer {
|
|||
}
|
||||
report.add_test_suite(test_suite);
|
||||
}
|
||||
writeln!(stdout, "{}", report.to_string().unwrap())?;
|
||||
writeln!(writer, "{}", report.to_string().unwrap())?;
|
||||
}
|
||||
SerializationFormat::Text => {
|
||||
for message in &diagnostics.messages {
|
||||
print_message(&mut stdout, message, self.autofix_level)?;
|
||||
print_message(
|
||||
writer,
|
||||
message,
|
||||
self.autofix_level,
|
||||
&diagnostics.jupyter_index,
|
||||
)?;
|
||||
}
|
||||
if self.flags.contains(Flags::SHOW_FIXES) {
|
||||
if !diagnostics.fixed.is_empty() {
|
||||
writeln!(stdout)?;
|
||||
print_fixed(&mut stdout, &diagnostics.fixed)?;
|
||||
writeln!(stdout)?;
|
||||
writeln!(writer)?;
|
||||
print_fixed(writer, &diagnostics.fixed)?;
|
||||
writeln!(writer)?;
|
||||
}
|
||||
}
|
||||
self.post_text(&mut stdout, diagnostics)?;
|
||||
self.post_text(writer, diagnostics)?;
|
||||
}
|
||||
SerializationFormat::Grouped => {
|
||||
for (filename, messages) in group_messages_by_filename(&diagnostics.messages) {
|
||||
|
@ -283,46 +295,57 @@ impl Printer {
|
|||
);
|
||||
|
||||
// Print the filename.
|
||||
writeln!(stdout, "{}:", relativize_path(filename).underline())?;
|
||||
writeln!(writer, "{}:", relativize_path(filename).underline())?;
|
||||
|
||||
// Print each message.
|
||||
for message in messages {
|
||||
print_grouped_message(
|
||||
&mut stdout,
|
||||
writer,
|
||||
message,
|
||||
self.autofix_level,
|
||||
row_length,
|
||||
column_length,
|
||||
diagnostics.jupyter_index.get(filename),
|
||||
)?;
|
||||
}
|
||||
writeln!(stdout)?;
|
||||
writeln!(writer)?;
|
||||
}
|
||||
if self.flags.contains(Flags::SHOW_FIXES) {
|
||||
if !diagnostics.fixed.is_empty() {
|
||||
writeln!(stdout)?;
|
||||
print_fixed(&mut stdout, &diagnostics.fixed)?;
|
||||
writeln!(stdout)?;
|
||||
writeln!(writer)?;
|
||||
print_fixed(writer, &diagnostics.fixed)?;
|
||||
writeln!(writer)?;
|
||||
}
|
||||
}
|
||||
self.post_text(&mut stdout, diagnostics)?;
|
||||
self.post_text(writer, diagnostics)?;
|
||||
}
|
||||
SerializationFormat::Github => {
|
||||
// Generate error workflow command in GitHub Actions format.
|
||||
// See: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
|
||||
for message in &diagnostics.messages {
|
||||
let row_column = if diagnostics.jupyter_index.contains_key(&message.filename) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
"1:0".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{}{}{}",
|
||||
message.location.row(),
|
||||
":",
|
||||
message.location.column(),
|
||||
)
|
||||
};
|
||||
let label = format!(
|
||||
"{}{}{}{}{}{} {} {}",
|
||||
"{}{}{}{} {} {}",
|
||||
relativize_path(&message.filename),
|
||||
":",
|
||||
message.location.row(),
|
||||
":",
|
||||
message.location.column(),
|
||||
row_column,
|
||||
":",
|
||||
message.kind.rule().noqa_code(),
|
||||
message.kind.body,
|
||||
);
|
||||
writeln!(
|
||||
stdout,
|
||||
writer,
|
||||
"::error title=Ruff \
|
||||
({}),file={},line={},col={},endLine={},endColumn={}::{}",
|
||||
message.kind.rule().noqa_code(),
|
||||
|
@ -339,13 +362,26 @@ impl Printer {
|
|||
// Generate JSON with violations in GitLab CI format
|
||||
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
|
||||
let project_dir = env::var("CI_PROJECT_DIR").ok();
|
||||
writeln!(stdout,
|
||||
writeln!(writer,
|
||||
"{}",
|
||||
serde_json::to_string_pretty(
|
||||
&diagnostics
|
||||
.messages
|
||||
.iter()
|
||||
.map(|message| {
|
||||
let lines = if diagnostics.jupyter_index.contains_key(&message.filename) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
json!({
|
||||
"begin": 1,
|
||||
"end": 1
|
||||
})
|
||||
} else {
|
||||
json!({
|
||||
"begin": message.location.row(),
|
||||
"end": message.end_location.row()
|
||||
})
|
||||
};
|
||||
json!({
|
||||
"description": format!("({}) {}", message.kind.rule().noqa_code(), message.kind.body),
|
||||
"severity": "major",
|
||||
|
@ -355,10 +391,7 @@ impl Printer {
|
|||
|| relativize_path(&message.filename),
|
||||
|project_dir| relativize_path_to(&message.filename, project_dir),
|
||||
),
|
||||
"lines": {
|
||||
"begin": message.location.row(),
|
||||
"end": message.end_location.row()
|
||||
}
|
||||
"lines": lines
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -371,27 +404,44 @@ impl Printer {
|
|||
// Generate violations in Pylint format.
|
||||
// See: https://flake8.pycqa.org/en/latest/internal/formatters.html#pylint-formatter
|
||||
for message in &diagnostics.messages {
|
||||
let row = if diagnostics.jupyter_index.contains_key(&message.filename) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
1
|
||||
} else {
|
||||
message.location.row()
|
||||
};
|
||||
let label = format!(
|
||||
"{}:{}: [{}] {}",
|
||||
relativize_path(&message.filename),
|
||||
message.location.row(),
|
||||
row,
|
||||
message.kind.rule().noqa_code(),
|
||||
message.kind.body,
|
||||
);
|
||||
writeln!(stdout, "{label}")?;
|
||||
writeln!(writer, "{label}")?;
|
||||
}
|
||||
}
|
||||
SerializationFormat::Azure => {
|
||||
// Generate error logging commands for Azure Pipelines format.
|
||||
// See https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#logissue-log-an-error-or-warning
|
||||
for message in &diagnostics.messages {
|
||||
let row_column = if diagnostics.jupyter_index.contains_key(&message.filename) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
"linenumber=1;columnnumber=0".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"linenumber={};columnnumber={}",
|
||||
message.location.row(),
|
||||
message.location.column(),
|
||||
)
|
||||
};
|
||||
writeln!(
|
||||
stdout,
|
||||
writer,
|
||||
"##vso[task.logissue type=error\
|
||||
;sourcepath={};linenumber={};columnnumber={};code={};]{}",
|
||||
;sourcepath={};{};code={};]{}",
|
||||
message.filename,
|
||||
message.location.row(),
|
||||
message.location.column(),
|
||||
row_column,
|
||||
message.kind.rule().noqa_code(),
|
||||
message.kind.body,
|
||||
)?;
|
||||
|
@ -399,7 +449,7 @@ impl Printer {
|
|||
}
|
||||
}
|
||||
|
||||
stdout.flush()?;
|
||||
writer.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -522,7 +572,12 @@ impl Printer {
|
|||
writeln!(stdout)?;
|
||||
}
|
||||
for message in &diagnostics.messages {
|
||||
print_message(&mut stdout, message, self.autofix_level)?;
|
||||
print_message(
|
||||
&mut stdout,
|
||||
message,
|
||||
self.autofix_level,
|
||||
&diagnostics.jupyter_index,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
stdout.flush()?;
|
||||
|
@ -605,16 +660,33 @@ fn print_message<T: Write>(
|
|||
stdout: &mut T,
|
||||
message: &Message,
|
||||
autofix_level: fix::FixMode,
|
||||
jupyter_index: &FxHashMap<String, JupyterIndex>,
|
||||
) -> Result<()> {
|
||||
let label = format!(
|
||||
"{path}{sep}{row}{sep}{col}{sep} {code_and_body}",
|
||||
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
|
||||
let pos_label = if let Some(jupyter_index) = jupyter_index.get(&message.filename) {
|
||||
format!(
|
||||
"cell {cell}{sep}{row}{sep}{col}{sep}",
|
||||
cell = jupyter_index.row_to_cell[message.location.row()],
|
||||
sep = ":".cyan(),
|
||||
row = jupyter_index.row_to_row_in_cell[message.location.row()],
|
||||
col = message.location.column(),
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{row}{sep}{col}{sep}",
|
||||
sep = ":".cyan(),
|
||||
row = message.location.row(),
|
||||
col = message.location.column(),
|
||||
)
|
||||
};
|
||||
writeln!(
|
||||
stdout,
|
||||
"{path}{sep}{pos_label} {code_and_body}",
|
||||
path = relativize_path(&message.filename).bold(),
|
||||
sep = ":".cyan(),
|
||||
row = message.location.row(),
|
||||
col = message.location.column(),
|
||||
pos_label = pos_label,
|
||||
code_and_body = CodeAndBody(message, autofix_level)
|
||||
);
|
||||
writeln!(stdout, "{label}")?;
|
||||
)?;
|
||||
if let Some(source) = &message.source {
|
||||
let suggestion = message.kind.suggestion.clone();
|
||||
let footer = if suggestion.is_some() {
|
||||
|
@ -710,13 +782,29 @@ fn print_grouped_message<T: Write>(
|
|||
autofix_level: fix::FixMode,
|
||||
row_length: usize,
|
||||
column_length: usize,
|
||||
jupyter_index: Option<&JupyterIndex>,
|
||||
) -> Result<()> {
|
||||
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
|
||||
let pos_label = if let Some(jupyter_index) = jupyter_index {
|
||||
format!(
|
||||
"cell {cell}{sep}{row}{sep}{col}",
|
||||
cell = jupyter_index.row_to_cell[message.location.row()],
|
||||
sep = ":".cyan(),
|
||||
row = jupyter_index.row_to_row_in_cell[message.location.row()],
|
||||
col = message.location.column(),
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{row}{sep}{col}",
|
||||
sep = ":".cyan(),
|
||||
row = message.location.row(),
|
||||
col = message.location.column(),
|
||||
)
|
||||
};
|
||||
let label = format!(
|
||||
" {row_padding}{row}{sep}{col}{col_padding} {code_and_body}",
|
||||
" {row_padding}{pos_label}{col_padding} {code_and_body}",
|
||||
row_padding = " ".repeat(row_length - num_digits(message.location.row())),
|
||||
row = message.location.row(),
|
||||
sep = ":".cyan(),
|
||||
col = message.location.column(),
|
||||
pos_label = pos_label,
|
||||
col_padding = " ".repeat(column_length - num_digits(message.location.column())),
|
||||
code_and_body = CodeAndBody(message, autofix_level),
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue