Compare commits
3 Commits
alex/subsc
...
amy/ruffen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0fa410a02 | ||
|
|
06440dc5ba | ||
|
|
64fd7e900d |
@@ -31,6 +31,7 @@ ruff_options_metadata = { workspace = true, features = ["serde"] }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_formatter = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
ruff_python_trivia = { workspace = true }
|
||||
ruff_server = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
|
||||
@@ -11,6 +11,7 @@ use itertools::Itertools;
|
||||
use log::{error, warn};
|
||||
use rayon::iter::Either::{Left, Right};
|
||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
use regex::{Captures, Regex};
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, DisplayDiagnosticConfig, Severity, Span,
|
||||
};
|
||||
@@ -18,6 +19,7 @@ use ruff_linter::message::{EmitterContext, create_panic_diagnostic, render_diagn
|
||||
use ruff_linter::settings::types::OutputFormat;
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_python_parser::ParseError;
|
||||
use ruff_python_trivia::textwrap::{dedent, indent};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use thiserror::Error;
|
||||
use tracing::debug;
|
||||
@@ -489,6 +491,66 @@ pub(crate) fn format_source(
|
||||
formatted,
|
||||
)))
|
||||
}
|
||||
SourceKind::Markdown(unformatted_document) => {
|
||||
// adapted from blacken-docs
|
||||
// https://github.com/adamchainz/blacken-docs/blob/fb107c1dce25f9206e29297aaa1ed7afc2980a5a/src/blacken_docs/__init__.py#L17
|
||||
let code_block_regex = Regex::new(
|
||||
r"(?imsx)
|
||||
(?<before>
|
||||
^(?<indent>\ *)```[^\S\r\n]*
|
||||
(?:python|py|python3|py3)
|
||||
(?:\ .*?)?\n
|
||||
)
|
||||
(?<code>.*?)
|
||||
(?<after>
|
||||
^\ *```[^\S\r\n]*$
|
||||
)
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut changed = false;
|
||||
let formatted_document =
|
||||
code_block_regex.replace_all(unformatted_document, |capture: &Captures| {
|
||||
let (original, [before, code_indent, unformatted_code, after]) =
|
||||
capture.extract();
|
||||
|
||||
let unformatted_code = dedent(unformatted_code);
|
||||
let options = settings.to_format_options(source_type, &unformatted_code, path);
|
||||
|
||||
let formatted_code = if let Some(_range) = range {
|
||||
unimplemented!()
|
||||
} else {
|
||||
// Using `Printed::into_code` requires adding `ruff_formatter` as a direct dependency, and I suspect that Rust can optimize the closure away regardless.
|
||||
#[expect(clippy::redundant_closure_for_method_calls)]
|
||||
format_module_source(&unformatted_code, options)
|
||||
.map(|formatted| formatted.into_code())
|
||||
};
|
||||
|
||||
// TODO: figure out how to properly raise errors from inside closure
|
||||
if let Ok(formatted_code) = formatted_code {
|
||||
if formatted_code.len() == unformatted_code.len()
|
||||
&& formatted_code == *unformatted_code
|
||||
{
|
||||
original.to_string()
|
||||
} else {
|
||||
changed = true;
|
||||
let formatted_code = indent(formatted_code.as_str(), code_indent);
|
||||
format!("{before}{formatted_code}{after}")
|
||||
}
|
||||
} else {
|
||||
original.to_string()
|
||||
}
|
||||
});
|
||||
|
||||
if changed {
|
||||
Ok(FormattedSource::Formatted(SourceKind::Markdown(
|
||||
formatted_document.to_string(),
|
||||
)))
|
||||
} else {
|
||||
Ok(FormattedSource::Unchanged)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ pub enum SourceKind {
|
||||
Python(String),
|
||||
/// The source contains a Jupyter notebook.
|
||||
IpyNotebook(Box<Notebook>),
|
||||
/// The source contains Markdown text.
|
||||
Markdown(String),
|
||||
}
|
||||
|
||||
impl SourceKind {
|
||||
@@ -33,6 +35,7 @@ impl SourceKind {
|
||||
match self {
|
||||
SourceKind::IpyNotebook(notebook) => Some(notebook),
|
||||
SourceKind::Python(_) => None,
|
||||
SourceKind::Markdown(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,20 +43,36 @@ impl SourceKind {
|
||||
match self {
|
||||
SourceKind::Python(code) => Some(code),
|
||||
SourceKind::IpyNotebook(_) => None,
|
||||
SourceKind::Markdown(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_markdown(&self) -> Option<&str> {
|
||||
match self {
|
||||
SourceKind::Markdown(code) => Some(code),
|
||||
SourceKind::Python(_) => None,
|
||||
SourceKind::IpyNotebook(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_python(self) -> String {
|
||||
match self {
|
||||
SourceKind::Python(code) => code,
|
||||
SourceKind::IpyNotebook(_) => panic!("expected python code"),
|
||||
_ => panic!("expected python code"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_ipy_notebook(self) -> Notebook {
|
||||
match self {
|
||||
SourceKind::IpyNotebook(notebook) => *notebook,
|
||||
SourceKind::Python(_) => panic!("expected ipy notebook"),
|
||||
_ => panic!("expected ipy notebook"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_markdown(self) -> String {
|
||||
match self {
|
||||
SourceKind::Markdown(code) => code,
|
||||
_ => panic!("expected markdown text"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +85,7 @@ impl SourceKind {
|
||||
SourceKind::IpyNotebook(cloned)
|
||||
}
|
||||
SourceKind::Python(_) => SourceKind::Python(new_source),
|
||||
SourceKind::Markdown(_) => SourceKind::Markdown(new_source),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,20 +94,28 @@ impl SourceKind {
|
||||
match self {
|
||||
SourceKind::Python(source) => source,
|
||||
SourceKind::IpyNotebook(notebook) => notebook.source_code(),
|
||||
SourceKind::Markdown(source) => source,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the [`SourceKind`] from the given path. Returns `None` if the source is not a Python
|
||||
/// source file.
|
||||
pub fn from_path(path: &Path, source_type: PySourceType) -> Result<Option<Self>, SourceError> {
|
||||
if source_type.is_ipynb() {
|
||||
let notebook = Notebook::from_path(path)?;
|
||||
Ok(notebook
|
||||
.is_python_notebook()
|
||||
.then_some(Self::IpyNotebook(Box::new(notebook))))
|
||||
} else {
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
Ok(Some(Self::Python(contents)))
|
||||
match source_type {
|
||||
PySourceType::Ipynb => {
|
||||
let notebook = Notebook::from_path(path)?;
|
||||
Ok(notebook
|
||||
.is_python_notebook()
|
||||
.then_some(Self::IpyNotebook(Box::new(notebook))))
|
||||
}
|
||||
PySourceType::Markdown => {
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
Ok(Some(Self::Markdown(contents)))
|
||||
}
|
||||
PySourceType::Python | PySourceType::Stub => {
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
Ok(Some(Self::Python(contents)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +148,10 @@ impl SourceKind {
|
||||
notebook.write(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
SourceKind::Markdown(source) => {
|
||||
writer.write_all(source.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +172,10 @@ impl SourceKind {
|
||||
kind: DiffKind::IpyNotebook(src, dst),
|
||||
path,
|
||||
}),
|
||||
(SourceKind::Markdown(src), SourceKind::Markdown(dst)) => Some(SourceKindDiff {
|
||||
kind: DiffKind::Markdown(src, dst),
|
||||
path,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -212,6 +248,17 @@ impl std::fmt::Display for SourceKindDiff<'_> {
|
||||
|
||||
writeln!(f)?;
|
||||
}
|
||||
DiffKind::Markdown(original, modified) => {
|
||||
let mut diff = CodeDiff::new(original, modified);
|
||||
|
||||
let relative_path = self.path.map(fs::relativize_path);
|
||||
|
||||
if let Some(relative_path) = &relative_path {
|
||||
diff.header(relative_path, relative_path);
|
||||
}
|
||||
|
||||
writeln!(f, "{diff}")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -222,6 +269,7 @@ impl std::fmt::Display for SourceKindDiff<'_> {
|
||||
enum DiffKind<'a> {
|
||||
Python(&'a str, &'a str),
|
||||
IpyNotebook(&'a Notebook, &'a Notebook),
|
||||
Markdown(&'a str, &'a str),
|
||||
}
|
||||
|
||||
struct CodeDiff<'a> {
|
||||
|
||||
@@ -89,6 +89,8 @@ pub enum PySourceType {
|
||||
Stub,
|
||||
/// The source is a Jupyter notebook (`.ipynb`).
|
||||
Ipynb,
|
||||
/// The source is a Markdown file (`.md`).
|
||||
Markdown,
|
||||
}
|
||||
|
||||
impl PySourceType {
|
||||
@@ -106,6 +108,7 @@ impl PySourceType {
|
||||
"pyi" => Self::Stub,
|
||||
"pyw" => Self::Python,
|
||||
"ipynb" => Self::Ipynb,
|
||||
"md" => Self::Markdown,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
@@ -134,6 +137,10 @@ impl PySourceType {
|
||||
pub const fn is_ipynb(self) -> bool {
|
||||
matches!(self, Self::Ipynb)
|
||||
}
|
||||
|
||||
pub const fn is_markdown(self) -> bool {
|
||||
matches!(self, Self::Markdown)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: AsRef<Path>> From<P> for PySourceType {
|
||||
|
||||
@@ -334,7 +334,7 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLines {
|
||||
PySourceType::Stub => {
|
||||
write!(f, [empty_line()])
|
||||
}
|
||||
PySourceType::Python | PySourceType::Ipynb => {
|
||||
PySourceType::Python | PySourceType::Ipynb | PySourceType::Markdown => {
|
||||
write!(f, [empty_line(), empty_line()])
|
||||
}
|
||||
},
|
||||
|
||||
@@ -283,7 +283,9 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||
PySourceType::Stub => {
|
||||
empty_line().fmt(f)?;
|
||||
}
|
||||
PySourceType::Python | PySourceType::Ipynb => {
|
||||
PySourceType::Python
|
||||
| PySourceType::Ipynb
|
||||
| PySourceType::Markdown => {
|
||||
write!(f, [empty_line(), empty_line()])?;
|
||||
}
|
||||
},
|
||||
@@ -324,7 +326,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||
PySourceType::Stub => {
|
||||
empty_line().fmt(f)?;
|
||||
}
|
||||
PySourceType::Python | PySourceType::Ipynb => {
|
||||
PySourceType::Python | PySourceType::Ipynb | PySourceType::Markdown => {
|
||||
write!(f, [empty_line(), empty_line()])?;
|
||||
}
|
||||
},
|
||||
@@ -376,7 +378,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||
PySourceType::Stub => {
|
||||
empty_line().fmt(f)?;
|
||||
}
|
||||
PySourceType::Python | PySourceType::Ipynb => {
|
||||
PySourceType::Python | PySourceType::Ipynb | PySourceType::Markdown => {
|
||||
write!(f, [empty_line(), empty_line()])?;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -525,7 +525,7 @@ pub trait AsMode {
|
||||
impl AsMode for PySourceType {
|
||||
fn as_mode(&self) -> Mode {
|
||||
match self {
|
||||
PySourceType::Python | PySourceType::Stub => Mode::Module,
|
||||
PySourceType::Python | PySourceType::Stub | PySourceType::Markdown => Mode::Module,
|
||||
PySourceType::Ipynb => Mode::Ipython,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,6 +318,7 @@ impl EmbeddedFilePath<'_> {
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Python) => "mdtest_snippet.py",
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Stub) => "mdtest_snippet.pyi",
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Ipynb) => "mdtest_snippet.ipynb",
|
||||
EmbeddedFilePath::Autogenerated(PySourceType::Markdown) => "mdtest_snippet.md",
|
||||
EmbeddedFilePath::Explicit(path) => path,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user