mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-28 12:55:05 +00:00

This PR does the plumbing to make a new formatting option,
`docstring-code-format`, available in the configuration for end users.
It is disabled by default (opt-in). It is opt-in at least initially to
reflect a conservative posture. The intent is to make it opt-out at some
point in the future.
This was split out from #8811 in order to make #8811 easier to merge.
Namely, once this is merged, docstring code snippet formatting will
become available to end users. (See comments below for how we arrived at
the name.)
Closes #7146
## Test Plan
Other than the standard test suite, I ran the formatter over the CPython
and polars projects to ensure both that the result looked sensible and
that tests still passed. At time of writing, one issue that currently
appears is that reformatting code snippets trips the long line lint:
1905886802
1389 lines
37 KiB
Rust
1389 lines
37 KiB
Rust
#![cfg(not(target_family = "wasm"))]
|
|
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
use std::str;
|
|
|
|
use anyhow::Result;
|
|
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
|
|
use tempfile::TempDir;
|
|
|
|
const BIN_NAME: &str = "ruff";
|
|
|
|
#[test]
|
|
fn default_options() {
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.args(["format", "--isolated", "--stdin-filename", "test.py"])
|
|
.arg("-")
|
|
.pass_stdin(r#"
|
|
def foo(arg1, arg2,):
|
|
print('Should\'t change quotes')
|
|
|
|
|
|
if condition:
|
|
|
|
print('Hy "Micha"') # Should not change quotes
|
|
|
|
"#), @r###"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
def foo(
|
|
arg1,
|
|
arg2,
|
|
):
|
|
print("Should't change quotes")
|
|
|
|
|
|
if condition:
|
|
print('Hy "Micha"') # Should not change quotes
|
|
|
|
----- stderr -----
|
|
"###);
|
|
}
|
|
|
|
#[test]
|
|
fn default_files() -> Result<()> {
|
|
let tempdir = TempDir::new()?;
|
|
fs::write(
|
|
tempdir.path().join("foo.py"),
|
|
r#"
|
|
foo = "needs formatting"
|
|
"#,
|
|
)?;
|
|
fs::write(
|
|
tempdir.path().join("bar.py"),
|
|
r#"
|
|
bar = "needs formatting"
|
|
"#,
|
|
)?;
|
|
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.args(["format", "--isolated", "--no-cache", "--check"]).current_dir(tempdir.path()), @r###"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
Would reformat: bar.py
|
|
Would reformat: foo.py
|
|
2 files would be reformatted
|
|
|
|
----- stderr -----
|
|
"###);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn format_warn_stdin_filename_with_files() {
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.args(["format", "--isolated", "--stdin-filename", "foo.py"])
|
|
.arg("foo.py")
|
|
.pass_stdin("foo = 1"), @r###"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
foo = 1
|
|
|
|
----- stderr -----
|
|
warning: Ignoring file foo.py in favor of standard input.
|
|
"###);
|
|
}
|
|
|
|
#[test]
|
|
fn format_options() -> Result<()> {
|
|
let tempdir = TempDir::new()?;
|
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
|
fs::write(
|
|
&ruff_toml,
|
|
r#"
|
|
indent-width = 8
|
|
line-length = 84
|
|
|
|
[format]
|
|
indent-style = "tab"
|
|
quote-style = "single"
|
|
skip-magic-trailing-comma = true
|
|
line-ending = "cr-lf"
|
|
"#,
|
|
)?;
|
|
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.args(["format", "--config"])
|
|
.arg(&ruff_toml)
|
|
.arg("-")
|
|
.pass_stdin(r#"
|
|
def foo(arg1, arg2,):
|
|
print("Shouldn't change quotes. It exceeds the line width with the tab size 8")
|
|
|
|
|
|
if condition:
|
|
|
|
print("Should change quotes")
|
|
|
|
"#), @r###"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
def foo(arg1, arg2):
|
|
print(
|
|
"Shouldn't change quotes. It exceeds the line width with the tab size 8"
|
|
)
|
|
|
|
|
|
if condition:
|
|
print('Should change quotes')
|
|
|
|
----- stderr -----
|
|
"###);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn docstring_options() -> Result<()> {
|
|
let tempdir = TempDir::new()?;
|
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
|
fs::write(
|
|
&ruff_toml,
|
|
r#"
|
|
[format]
|
|
docstring-code-format = true
|
|
docstring-code-line-length = 20
|
|
"#,
|
|
)?;
|
|
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.args(["format", "--config"])
|
|
.arg(&ruff_toml)
|
|
.arg("-")
|
|
.pass_stdin(r#"
|
|
def f(x):
|
|
'''
|
|
Something about `f`. And an example:
|
|
|
|
.. code-block:: python
|
|
|
|
foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear)
|
|
|
|
Another example:
|
|
|
|
```py
|
|
foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear)
|
|
```
|
|
|
|
And another:
|
|
|
|
>>> foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear)
|
|
'''
|
|
pass
|
|
"#), @r###"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
def f(x):
|
|
"""
|
|
Something about `f`. And an example:
|
|
|
|
.. code-block:: python
|
|
|
|
(
|
|
foo,
|
|
bar,
|
|
quux,
|
|
) = this_is_a_long_line(
|
|
lion,
|
|
hippo,
|
|
lemur,
|
|
bear,
|
|
)
|
|
|
|
Another example:
|
|
|
|
```py
|
|
(
|
|
foo,
|
|
bar,
|
|
quux,
|
|
) = this_is_a_long_line(
|
|
lion,
|
|
hippo,
|
|
lemur,
|
|
bear,
|
|
)
|
|
```
|
|
|
|
And another:
|
|
|
|
>>> (
|
|
... foo,
|
|
... bar,
|
|
... quux,
|
|
... ) = this_is_a_long_line(
|
|
... lion,
|
|
... hippo,
|
|
... lemur,
|
|
... bear,
|
|
... )
|
|
"""
|
|
pass
|
|
|
|
----- stderr -----
|
|
"###);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_line_endings() -> Result<()> {
|
|
let tempdir = TempDir::new()?;
|
|
|
|
fs::write(
|
|
tempdir.path().join("main.py"),
|
|
"from test import say_hy\n\nif __name__ == \"__main__\":\n say_hy(\"dear Ruff contributor\")\n",
|
|
)?;
|
|
|
|
fs::write(
|
|
tempdir.path().join("test.py"),
|
|
"def say_hy(name: str):\r\n print(f\"Hy {name}\")\r\n",
|
|
)?;
|
|
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.current_dir(tempdir.path())
|
|
.args(["format", "--no-cache", "--diff", "--isolated"])
|
|
.arg("."), @r###"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
|
|
----- stderr -----
|
|
2 files left unchanged
|
|
"###);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn exclude() -> Result<()> {
|
|
let tempdir = TempDir::new()?;
|
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
|
fs::write(
|
|
&ruff_toml,
|
|
r#"
|
|
extend-exclude = ["out"]
|
|
|
|
[format]
|
|
exclude = ["test.py", "generated.py"]
|
|
"#,
|
|
)?;
|
|
|
|
fs::write(
|
|
tempdir.path().join("main.py"),
|
|
r#"
|
|
from test import say_hy
|
|
|
|
if __name__ == "__main__":
|
|
say_hy("dear Ruff contributor")
|
|
"#,
|
|
)?;
|
|
|
|
// Excluded file but passed to the CLI directly, should be formatted
|
|
let test_path = tempdir.path().join("test.py");
|
|
fs::write(
|
|
&test_path,
|
|
r#"
|
|
def say_hy(name: str):
|
|
print(f"Hy {name}")"#,
|
|
)?;
|
|
|
|
fs::write(
|
|
tempdir.path().join("generated.py"),
|
|
r#"NUMBERS = [
|
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
|
|
10, 11, 12, 13, 14, 15, 16, 17, 18, 19
|
|
]
|
|
OTHER = "OTHER"
|
|
"#,
|
|
)?;
|
|
|
|
let out_dir = tempdir.path().join("out");
|
|
fs::create_dir(&out_dir)?;
|
|
|
|
fs::write(out_dir.join("a.py"), "a = a")?;
|
|
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.current_dir(tempdir.path())
|
|
.args(["format", "--no-cache", "--check", "--config"])
|
|
.arg(ruff_toml.file_name().unwrap())
|
|
// Explicitly pass test.py, should be formatted regardless of it being excluded by format.exclude
|
|
.arg(test_path.file_name().unwrap())
|
|
// Format all other files in the directory, should respect the `exclude` and `format.exclude` options
|
|
.arg("."), @r###"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
Would reformat: main.py
|
|
Would reformat: test.py
|
|
2 files would be reformatted
|
|
|
|
----- stderr -----
|
|
"###);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn force_exclude() -> Result<()> {
|
|
let tempdir = TempDir::new()?;
|
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
|
fs::write(
|
|
&ruff_toml,
|
|
r#"
|
|
extend-exclude = ["out"]
|
|
|
|
[format]
|
|
exclude = ["test.py", "generated.py"]
|
|
"#,
|
|
)?;
|
|
|
|
fs::write(
|
|
tempdir.path().join("main.py"),
|
|
r#"
|
|
from test import say_hy
|
|
|
|
if __name__ == "__main__":
|
|
say_hy("dear Ruff contributor")
|
|
"#,
|
|
)?;
|
|
|
|
// Excluded file but passed to the CLI directly, should be formatted
|
|
let test_path = tempdir.path().join("test.py");
|
|
fs::write(
|
|
&test_path,
|
|
r#"
|
|
def say_hy(name: str):
|
|
print(f"Hy {name}")"#,
|
|
)?;
|
|
|
|
fs::write(
|
|
tempdir.path().join("generated.py"),
|
|
r#"NUMBERS = [
|
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
|
|
10, 11, 12, 13, 14, 15, 16, 17, 18, 19
|
|
]
|
|
OTHER = "OTHER"
|
|
"#,
|
|
)?;
|
|
|
|
let out_dir = tempdir.path().join("out");
|
|
fs::create_dir(&out_dir)?;
|
|
|
|
fs::write(out_dir.join("a.py"), "a = a")?;
|
|
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.current_dir(tempdir.path())
|
|
.args(["format", "--no-cache", "--force-exclude", "--check", "--config"])
|
|
.arg(ruff_toml.file_name().unwrap())
|
|
// Explicitly pass test.py, should be respect the `format.exclude` when `--force-exclude` is present
|
|
.arg(test_path.file_name().unwrap())
|
|
// Format all other files in the directory, should respect the `exclude` and `format.exclude` options
|
|
.arg("."), @r###"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
Would reformat: main.py
|
|
1 file would be reformatted
|
|
|
|
----- stderr -----
|
|
"###);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn exclude_stdin() -> Result<()> {
|
|
let tempdir = TempDir::new()?;
|
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
|
fs::write(
|
|
&ruff_toml,
|
|
r#"
|
|
extend-select = ["B", "Q"]
|
|
ignore = ["Q000", "Q001", "Q002", "Q003"]
|
|
|
|
[format]
|
|
exclude = ["generated.py"]
|
|
"#,
|
|
)?;
|
|
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.current_dir(tempdir.path())
|
|
.args(["format", "--config", &ruff_toml.file_name().unwrap().to_string_lossy(), "--stdin-filename", "generated.py", "-"])
|
|
.pass_stdin(r#"
|
|
from test import say_hy
|
|
|
|
if __name__ == '__main__':
|
|
say_hy("dear Ruff contributor")
|
|
"#), @r###"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
from test import say_hy
|
|
|
|
if __name__ == "__main__":
|
|
say_hy("dear Ruff contributor")
|
|
|
|
----- stderr -----
|
|
"###);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn force_exclude_stdin() -> Result<()> {
|
|
let tempdir = TempDir::new()?;
|
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
|
fs::write(
|
|
&ruff_toml,
|
|
r#"
|
|
extend-select = ["B", "Q"]
|
|
ignore = ["Q000", "Q001", "Q002", "Q003"]
|
|
|
|
[format]
|
|
exclude = ["generated.py"]
|
|
"#,
|
|
)?;
|
|
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.current_dir(tempdir.path())
|
|
.args(["format", "--config", &ruff_toml.file_name().unwrap().to_string_lossy(), "--stdin-filename", "generated.py", "--force-exclude", "-"])
|
|
.pass_stdin(r#"
|
|
from test import say_hy
|
|
|
|
if __name__ == '__main__':
|
|
say_hy("dear Ruff contributor")
|
|
"#), @r###"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
|
|
from test import say_hy
|
|
|
|
if __name__ == '__main__':
|
|
say_hy("dear Ruff contributor")
|
|
|
|
----- stderr -----
|
|
"###);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn format_option_inheritance() -> Result<()> {
|
|
let tempdir = TempDir::new()?;
|
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
|
let base_toml = tempdir.path().join("base.toml");
|
|
fs::write(
|
|
&ruff_toml,
|
|
r#"
|
|
extend = "base.toml"
|
|
|
|
[lint]
|
|
extend-select = ["COM812"]
|
|
|
|
[format]
|
|
quote-style = "single"
|
|
"#,
|
|
)?;
|
|
|
|
fs::write(
|
|
base_toml,
|
|
r#"
|
|
[format]
|
|
indent-style = "tab"
|
|
"#,
|
|
)?;
|
|
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.args(["format", "--config"])
|
|
.arg(&ruff_toml)
|
|
.arg("-")
|
|
.pass_stdin(r#"
|
|
def foo(arg1, arg2,):
|
|
print("Shouldn't change quotes")
|
|
|
|
|
|
if condition:
|
|
|
|
print("Should change quotes")
|
|
|
|
"#), @r###"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
def foo(
|
|
arg1,
|
|
arg2,
|
|
):
|
|
print("Shouldn't change quotes")
|
|
|
|
|
|
if condition:
|
|
print('Should change quotes')
|
|
|
|
----- stderr -----
|
|
warning: The following rules may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.
|
|
"###);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn deprecated_options() -> Result<()> {
|
|
let tempdir = TempDir::new()?;
|
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
|
fs::write(
|
|
&ruff_toml,
|
|
r"
|
|
tab-size = 2
|
|
",
|
|
)?;
|
|
|
|
insta::with_settings!({filters => vec![
|
|
(&*regex::escape(ruff_toml.to_str().unwrap()), "[RUFF-TOML-PATH]"),
|
|
]}, {
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.args(["format", "--config"])
|
|
.arg(&ruff_toml)
|
|
.arg("-")
|
|
.pass_stdin(r"
|
|
if True:
|
|
pass
|
|
"), @r###"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
if True:
|
|
pass
|
|
|
|
----- stderr -----
|
|
warning: The `tab-size` option has been renamed to `indent-width` to emphasize that it configures the indentation used by the formatter as well as the tab width. Please update your configuration to use `indent-width = <value>` instead.
|
|
"###);
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
/// Since 0.1.0 the legacy format option is no longer supported
|
|
#[test]
|
|
fn legacy_format_option() -> Result<()> {
|
|
let tempdir = TempDir::new()?;
|
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
|
fs::write(
|
|
&ruff_toml,
|
|
r#"
|
|
format = "json"
|
|
"#,
|
|
)?;
|
|
|
|
insta::with_settings!({filters => vec![
|
|
(&*regex::escape(ruff_toml.to_str().unwrap()), "[RUFF-TOML-PATH]"),
|
|
]}, {
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.args(["check", "--select", "F401", "--no-cache", "--config"])
|
|
.arg(&ruff_toml)
|
|
.arg("-")
|
|
.pass_stdin(r"
|
|
import os
|
|
"), @r###"
|
|
success: false
|
|
exit_code: 2
|
|
----- stdout -----
|
|
|
|
----- stderr -----
|
|
ruff failed
|
|
Cause: Failed to parse `[RUFF-TOML-PATH]`: TOML parse error at line 2, column 10
|
|
|
|
|
2 | format = "json"
|
|
| ^^^^^^
|
|
invalid type: string "json", expected struct FormatOptions
|
|
|
|
"###);
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn conflicting_options() -> Result<()> {
|
|
let tempdir = TempDir::new()?;
|
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
|
fs::write(
|
|
&ruff_toml,
|
|
r#"
|
|
indent-width = 2
|
|
|
|
[lint]
|
|
select = ["ALL"]
|
|
ignore = ["D203", "D212"]
|
|
|
|
[lint.isort]
|
|
lines-after-imports = 3
|
|
lines-between-types = 2
|
|
force-wrap-aliases = true
|
|
combine-as-imports = true
|
|
split-on-trailing-comma = true
|
|
|
|
[lint.flake8-quotes]
|
|
inline-quotes = "single"
|
|
docstring-quotes = "single"
|
|
multiline-quotes = "single"
|
|
|
|
[format]
|
|
skip-magic-trailing-comma = true
|
|
indent-style = "tab"
|
|
"#,
|
|
)?;
|
|
|
|
let test_path = tempdir.path().join("test.py");
|
|
fs::write(
|
|
&test_path,
|
|
r#"
|
|
def say_hy(name: str):
|
|
print(f"Hy {name}")"#,
|
|
)?;
|
|
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.args(["format", "--no-cache", "--config"])
|
|
.arg(&ruff_toml)
|
|
.arg(test_path), @r###"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
1 file reformatted
|
|
|
|
----- stderr -----
|
|
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.
|
|
warning: The `format.indent-style="tab"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
|
|
warning: The `format.indent-style="tab"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
|
|
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
|
|
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `"double"`.`
|
|
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `"double"`.`
|
|
warning: The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default).
|
|
warning: The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default).
|
|
warning: The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`.
|
|
warning: The isort option `isort.split-on-trailing-comma` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` or `format.skip-magic-trailing-comma=false`.
|
|
"###);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn conflicting_options_stdin() -> Result<()> {
|
|
let tempdir = TempDir::new()?;
|
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
|
fs::write(
|
|
&ruff_toml,
|
|
r#"
|
|
indent-width = 2
|
|
|
|
[lint]
|
|
select = ["ALL"]
|
|
ignore = ["D203", "D212"]
|
|
|
|
[lint.isort]
|
|
lines-after-imports = 3
|
|
lines-between-types = 2
|
|
force-wrap-aliases = true
|
|
combine-as-imports = true
|
|
split-on-trailing-comma = true
|
|
|
|
[lint.flake8-quotes]
|
|
inline-quotes = "single"
|
|
docstring-quotes = "single"
|
|
multiline-quotes = "single"
|
|
|
|
[format]
|
|
skip-magic-trailing-comma = true
|
|
indent-style = "tab"
|
|
"#,
|
|
)?;
|
|
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.args(["format", "--config"])
|
|
.arg(&ruff_toml)
|
|
.arg("-")
|
|
.pass_stdin(r#"
|
|
def say_hy(name: str):
|
|
print(f"Hy {name}")"#), @r###"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
def say_hy(name: str):
|
|
print(f"Hy {name}")
|
|
|
|
----- stderr -----
|
|
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.
|
|
warning: The `format.indent-style="tab"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
|
|
warning: The `format.indent-style="tab"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
|
|
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
|
|
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `"double"`.`
|
|
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `"double"`.`
|
|
warning: The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default).
|
|
warning: The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default).
|
|
warning: The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`.
|
|
warning: The isort option `isort.split-on-trailing-comma` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` or `format.skip-magic-trailing-comma=false`.
|
|
"###);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn valid_linter_options() -> Result<()> {
|
|
let tempdir = TempDir::new()?;
|
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
|
fs::write(
|
|
&ruff_toml,
|
|
r#"
|
|
[lint]
|
|
select = ["ALL"]
|
|
ignore = ["D203", "D212", "COM812", "ISC001"]
|
|
|
|
[lint.isort]
|
|
lines-after-imports = 2
|
|
lines-between-types = 1
|
|
force-wrap-aliases = true
|
|
combine-as-imports = true
|
|
split-on-trailing-comma = true
|
|
|
|
[lint.flake8-quotes]
|
|
inline-quotes = "single"
|
|
docstring-quotes = "double"
|
|
multiline-quotes = "double"
|
|
|
|
[format]
|
|
skip-magic-trailing-comma = false
|
|
quote-style = "single"
|
|
"#,
|
|
)?;
|
|
|
|
let test_path = tempdir.path().join("test.py");
|
|
fs::write(
|
|
&test_path,
|
|
r#"
|
|
def say_hy(name: str):
|
|
print(f"Hy {name}")"#,
|
|
)?;
|
|
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.args(["format", "--no-cache", "--config"])
|
|
.arg(&ruff_toml)
|
|
.arg(test_path), @r###"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
1 file reformatted
|
|
|
|
----- stderr -----
|
|
"###);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn all_rules_default_options() -> Result<()> {
|
|
let tempdir = TempDir::new()?;
|
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
|
|
|
fs::write(
|
|
&ruff_toml,
|
|
r#"
|
|
[lint]
|
|
select = ["ALL"]
|
|
"#,
|
|
)?;
|
|
|
|
let test_path = tempdir.path().join("test.py");
|
|
fs::write(
|
|
&test_path,
|
|
r#"
|
|
def say_hy(name: str):
|
|
print(f"Hy {name}")"#,
|
|
)?;
|
|
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.args(["format", "--no-cache", "--config"])
|
|
.arg(&ruff_toml)
|
|
.arg(test_path), @r###"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
1 file reformatted
|
|
|
|
----- stderr -----
|
|
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
|
|
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
|
|
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.
|
|
"###);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_diff() {
|
|
let args = ["format", "--no-cache", "--isolated", "--diff"];
|
|
let fixtures = Path::new("resources").join("test").join("fixtures");
|
|
let paths = [
|
|
fixtures.join("unformatted.py"),
|
|
fixtures.join("formatted.py"),
|
|
fixtures.join("unformatted.ipynb"),
|
|
];
|
|
insta::with_settings!({filters => vec![
|
|
// Replace windows paths
|
|
(r"\\", "/"),
|
|
]}, {
|
|
assert_cmd_snapshot!(
|
|
Command::new(get_cargo_bin(BIN_NAME)).args(args).args(paths),
|
|
@r###"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
--- resources/test/fixtures/unformatted.ipynb:cell 1
|
|
+++ resources/test/fixtures/unformatted.ipynb:cell 1
|
|
@@ -1,3 +1,4 @@
|
|
import numpy
|
|
-maths = (numpy.arange(100)**2).sum()
|
|
-stats= numpy.asarray([1,2,3,4]).median()
|
|
+
|
|
+maths = (numpy.arange(100) ** 2).sum()
|
|
+stats = numpy.asarray([1, 2, 3, 4]).median()
|
|
--- resources/test/fixtures/unformatted.ipynb:cell 3
|
|
+++ resources/test/fixtures/unformatted.ipynb:cell 3
|
|
@@ -1,4 +1,6 @@
|
|
# A cell with IPython escape command
|
|
def some_function(foo, bar):
|
|
pass
|
|
+
|
|
+
|
|
%matplotlib inline
|
|
--- resources/test/fixtures/unformatted.ipynb:cell 4
|
|
+++ resources/test/fixtures/unformatted.ipynb:cell 4
|
|
@@ -1,5 +1,10 @@
|
|
foo = %pwd
|
|
-def some_function(foo,bar,):
|
|
+
|
|
+
|
|
+def some_function(
|
|
+ foo,
|
|
+ bar,
|
|
+):
|
|
# Another cell with IPython escape command
|
|
foo = %pwd
|
|
print(foo)
|
|
|
|
--- resources/test/fixtures/unformatted.py
|
|
+++ resources/test/fixtures/unformatted.py
|
|
@@ -1,3 +1,3 @@
|
|
x = 1
|
|
-y=2
|
|
+y = 2
|
|
z = 3
|
|
|
|
|
|
----- stderr -----
|
|
2 files would be reformatted, 1 file left unchanged
|
|
"###);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_diff_no_change() {
|
|
let args = ["format", "--no-cache", "--isolated", "--diff"];
|
|
let fixtures = Path::new("resources").join("test").join("fixtures");
|
|
let paths = [fixtures.join("unformatted.py")];
|
|
insta::with_settings!({filters => vec![
|
|
// Replace windows paths
|
|
(r"\\", "/"),
|
|
]}, {
|
|
assert_cmd_snapshot!(
|
|
Command::new(get_cargo_bin(BIN_NAME)).args(args).args(paths),
|
|
@r###"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
--- resources/test/fixtures/unformatted.py
|
|
+++ resources/test/fixtures/unformatted.py
|
|
@@ -1,3 +1,3 @@
|
|
x = 1
|
|
-y=2
|
|
+y = 2
|
|
z = 3
|
|
|
|
|
|
----- stderr -----
|
|
1 file would be reformatted
|
|
"###
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_diff_stdin_unformatted() {
|
|
let args = [
|
|
"format",
|
|
"--isolated",
|
|
"--diff",
|
|
"-",
|
|
"--stdin-filename",
|
|
"unformatted.py",
|
|
];
|
|
let fixtures = Path::new("resources").join("test").join("fixtures");
|
|
let unformatted = fs::read(fixtures.join("unformatted.py")).unwrap();
|
|
assert_cmd_snapshot!(
|
|
Command::new(get_cargo_bin(BIN_NAME)).args(args).pass_stdin(unformatted),
|
|
@r###"
|
|
success: false
|
|
exit_code: 1
|
|
----- stdout -----
|
|
--- unformatted.py
|
|
+++ unformatted.py
|
|
@@ -1,3 +1,3 @@
|
|
x = 1
|
|
-y=2
|
|
+y = 2
|
|
z = 3
|
|
|
|
|
|
----- stderr -----
|
|
"###);
|
|
}
|
|
|
|
#[test]
|
|
fn test_diff_stdin_formatted() {
|
|
let args = ["format", "--isolated", "--diff", "-"];
|
|
let fixtures = Path::new("resources").join("test").join("fixtures");
|
|
let unformatted = fs::read(fixtures.join("formatted.py")).unwrap();
|
|
assert_cmd_snapshot!(
|
|
Command::new(get_cargo_bin(BIN_NAME)).args(args).pass_stdin(unformatted),
|
|
@r###"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
|
|
----- stderr -----
|
|
"###);
|
|
}
|
|
|
|
#[test]
|
|
fn test_notebook_trailing_semicolon() {
|
|
let fixtures = Path::new("resources").join("test").join("fixtures");
|
|
let unformatted = fs::read(fixtures.join("trailing_semicolon.ipynb")).unwrap();
|
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
|
.args(["format", "--isolated", "--stdin-filename", "test.ipynb"])
|
|
.arg("-")
|
|
.pass_stdin(unformatted), @r###"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"id": "4f8ce941-1492-4d4e-8ab5-70d733fe891a",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"%config ZMQInteractiveShell.ast_node_interactivity=\"last_expr_or_assign\""
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 2,
|
|
"id": "721ec705-0c65-4bfb-9809-7ed8bc534186",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"1"
|
|
]
|
|
},
|
|
"execution_count": 2,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"# Assignment statement without a semicolon\n",
|
|
"x = 1"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 3,
|
|
"id": "de50e495-17e5-41cc-94bd-565757555d7e",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Assignment statement with a semicolon\n",
|
|
"x = 1\n",
|
|
"x = 1;"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 4,
|
|
"id": "39e31201-23da-44eb-8684-41bba3663991",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"2"
|
|
]
|
|
},
|
|
"execution_count": 4,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"# Augmented assignment without a semicolon\n",
|
|
"x += 1"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 5,
|
|
"id": "6b73d3dd-c73a-4697-9e97-e109a6c1fbab",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Augmented assignment without a semicolon\n",
|
|
"x += 1\n",
|
|
"x += 1; # comment\n",
|
|
"# comment"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 6,
|
|
"id": "2a3e5b86-aa5b-46ba-b9c6-0386d876f58c",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Multiple assignment without a semicolon\n",
|
|
"x = y = 1"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 7,
|
|
"id": "07f89e51-9357-4cfb-8fc5-76fb75e35949",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Multiple assignment with a semicolon\n",
|
|
"x = y = 1\n",
|
|
"x = y = 1"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 8,
|
|
"id": "c22b539d-473e-48f8-a236-625e58c47a00",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Tuple unpacking without a semicolon\n",
|
|
"x, y = 1, 2"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 9,
|
|
"id": "12c87940-a0d5-403b-a81c-7507eb06dc7e",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Tuple unpacking with a semicolon (irrelevant)\n",
|
|
"x, y = 1, 2\n",
|
|
"x, y = 1, 2 # comment\n",
|
|
"# comment"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 10,
|
|
"id": "5a768c76-6bc4-470c-b37e-8cc14bc6caf4",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"1"
|
|
]
|
|
},
|
|
"execution_count": 10,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"# Annotated assignment statement without a semicolon\n",
|
|
"x: int = 1"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 11,
|
|
"id": "21bfda82-1a9a-4ba1-9078-74ac480804b5",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Annotated assignment statement without a semicolon\n",
|
|
"x: int = 1\n",
|
|
"x: int = 1; # comment\n",
|
|
"# comment"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 12,
|
|
"id": "09929999-ff29-4d10-ad2b-e665af15812d",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"1"
|
|
]
|
|
},
|
|
"execution_count": 12,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"# Assignment expression without a semicolon\n",
|
|
"(x := 1)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 13,
|
|
"id": "32a83217-1bad-4f61-855e-ffcdb119c763",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Assignment expression with a semicolon\n",
|
|
"(x := 1)\n",
|
|
"(x := 1); # comment\n",
|
|
"# comment"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 14,
|
|
"id": "61b81865-277e-4964-b03e-eb78f1f318eb",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"1"
|
|
]
|
|
},
|
|
"execution_count": 14,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"x = 1\n",
|
|
"# Expression without a semicolon\n",
|
|
"x"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 15,
|
|
"id": "974c29be-67e1-4000-95fa-6ca118a63bad",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"x = 1\n",
|
|
"# Expression with a semicolon\n",
|
|
"x;"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 16,
|
|
"id": "cfeb1757-46d6-4f13-969f-a283b6d0304f",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class Point:\n",
|
|
" def __init__(self, x, y):\n",
|
|
" self.x = x\n",
|
|
" self.y = y\n",
|
|
"\n",
|
|
"\n",
|
|
"p = Point(0, 0);"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 17,
|
|
"id": "2ee7f1a5-ccfe-4004-bfa4-ef834a58da97",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Assignment statement where the left is an attribute access doesn't\n",
|
|
"# print the value.\n",
|
|
"p.x = 1"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 18,
|
|
"id": "3e49370a-048b-474d-aa0a-3d1d4a73ad37",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"data = {}\n",
|
|
"\n",
|
|
"# Neither does the subscript node\n",
|
|
"data[\"foo\"] = 1"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 19,
|
|
"id": "d594bdd3-eaa9-41ef-8cda-cf01bc273b2d",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"if x := 1:\n",
|
|
" # It should be the top level statement\n",
|
|
" x"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 20,
|
|
"id": "e532f0cf-80c7-42b7-8226-6002fcf74fb6",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"1"
|
|
]
|
|
},
|
|
"execution_count": 20,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"# Parentheses with comments\n",
|
|
"(\n",
|
|
" x := 1 # comment\n",
|
|
") # comment"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 21,
|
|
"id": "473c5d62-871b-46ed-8a34-27095243f462",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Parentheses with comments\n",
|
|
"(\n",
|
|
" x := 1 # comment\n",
|
|
"); # comment"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 22,
|
|
"id": "8c3c2361-f49f-45fe-bbe3-7e27410a8a86",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"'Hello world!'"
|
|
]
|
|
},
|
|
"execution_count": 22,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"\"\"\"Hello world!\"\"\""
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 23,
|
|
"id": "23dbe9b5-3f68-4890-ab2d-ab0dbfd0712a",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"\"\"\"Hello world!\"\"\"; # comment\n",
|
|
"# comment"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 24,
|
|
"id": "3ce33108-d95d-4c70-83d1-0d4fd36a2951",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"'x = 1'"
|
|
]
|
|
},
|
|
"execution_count": 24,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"x = 1\n",
|
|
"f\"x = {x}\""
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 25,
|
|
"id": "654a4a67-de43-4684-824a-9451c67db48f",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"x = 1\n",
|
|
"f\"x = {x}\"\n",
|
|
"f\"x = {x}\"; # comment\n",
|
|
"# comment"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python (ruff-playground)",
|
|
"language": "python",
|
|
"name": "ruff-playground"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3.11.3"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5
|
|
}
|
|
|
|
----- stderr -----
|
|
"###);
|
|
}
|