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:
konstin 2023-03-20 12:06:01 +01:00 committed by GitHub
parent a45753f462
commit 81d0884974
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1035 additions and 68 deletions

View file

@ -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),
);