diff --git a/crates/ruff_cli/tests/format.rs b/crates/ruff_cli/tests/format.rs index e9970645d7b67..cd3e14629899e 100644 --- a/crates/ruff_cli/tests/format.rs +++ b/crates/ruff_cli/tests/format.rs @@ -139,6 +139,99 @@ if condition: 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()?; diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs index 8f3bc431ec815..4f637dca9ee10 100644 --- a/crates/ruff_python_formatter/src/options.rs +++ b/crates/ruff_python_formatter/src/options.rs @@ -175,6 +175,12 @@ impl PyFormatOptions { self } + #[must_use] + pub fn with_docstring_code_line_width(mut self, line_width: DocstringCodeLineWidth) -> Self { + self.docstring_code_line_width = line_width; + self + } + #[must_use] pub fn with_preview(mut self, preview: PreviewMode) -> Self { self.preview = preview; @@ -302,13 +308,14 @@ impl DocstringCode { } } -#[derive(Copy, Clone, Eq, PartialEq, CacheKey)] +#[derive(Copy, Clone, Default, Eq, PartialEq, CacheKey)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] #[cfg_attr(feature = "serde", serde(untagged))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum DocstringCodeLineWidth { Fixed(LineWidth), + #[default] #[cfg_attr( feature = "serde", serde(deserialize_with = "deserialize_docstring_code_line_width_dynamic") @@ -316,12 +323,6 @@ pub enum DocstringCodeLineWidth { Dynamic, } -impl Default for DocstringCodeLineWidth { - fn default() -> DocstringCodeLineWidth { - DocstringCodeLineWidth::Fixed(default_line_width()) - } -} - impl std::fmt::Debug for DocstringCodeLineWidth { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap index 98220dcff890a..bf8fa7f40df94 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap @@ -173,7 +173,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -347,7 +347,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -521,7 +521,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -695,7 +695,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -869,7 +869,7 @@ quote-style = Single line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap index b93825ea198e6..672b7a715d623 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap @@ -1366,7 +1366,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -2736,7 +2736,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -4106,7 +4106,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -5476,7 +5476,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -6846,7 +6846,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Enabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -7090,7 +7090,9 @@ def doctest_long_lines(): This won't get wrapped even though it exceeds our configured line width because it doesn't exceed the line width within this docstring. e.g, the `f` in `foo` is treated as the first column. - >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + >>> foo, bar, quux = this_is_a_long_line( + ... lion, giraffe, hippo, zeba, lemur, penguin, monkey + ... ) But this one is long enough to get wrapped. >>> foo, bar, quux = this_is_a_long_line( @@ -8211,7 +8213,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Enabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -8455,7 +8457,9 @@ def doctest_long_lines(): This won't get wrapped even though it exceeds our configured line width because it doesn't exceed the line width within this docstring. e.g, the `f` in `foo` is treated as the first column. - >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + >>> foo, bar, quux = this_is_a_long_line( + ... lion, giraffe, hippo, zeba, lemur, penguin, monkey + ... ) But this one is long enough to get wrapped. >>> foo, bar, quux = this_is_a_long_line( @@ -9576,7 +9580,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Enabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -9820,11 +9824,22 @@ def doctest_long_lines(): This won't get wrapped even though it exceeds our configured line width because it doesn't exceed the line width within this docstring. e.g, the `f` in `foo` is treated as the first column. - >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + >>> foo, bar, quux = this_is_a_long_line( + ... lion, giraffe, hippo, zeba, lemur, penguin, monkey + ... ) But this one is long enough to get wrapped. >>> foo, bar, quux = this_is_a_long_line( - ... lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard + ... lion, + ... giraffe, + ... hippo, + ... zeba, + ... lemur, + ... penguin, + ... monkey, + ... spider, + ... bear, + ... leopard, ... ) """ # This demostrates a normal line that will get wrapped but won't @@ -10941,7 +10956,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Enabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -11185,7 +11200,9 @@ def doctest_long_lines(): This won't get wrapped even though it exceeds our configured line width because it doesn't exceed the line width within this docstring. e.g, the `f` in `foo` is treated as the first column. - >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + >>> foo, bar, quux = this_is_a_long_line( + ... lion, giraffe, hippo, zeba, lemur, penguin, monkey + ... ) But this one is long enough to get wrapped. >>> foo, bar, quux = this_is_a_long_line( diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap index 347c3cc2a8388..9f7fd5b9ac5b7 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap @@ -25,7 +25,7 @@ quote-style = Double line-ending = CarriageReturnLineFeed magic-trailing-comma = Respect docstring-code = Enabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__bytes.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__bytes.py.snap index fc89a38635108..b741654c94c4a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__bytes.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__bytes.py.snap @@ -136,7 +136,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -288,7 +288,7 @@ quote-style = Single line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap index 0829244eb68b4..306a10a49f9fe 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap @@ -151,7 +151,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -327,7 +327,7 @@ quote-style = Single line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap index 2080aa4a52451..2c5ce6935feec 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap @@ -35,7 +35,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -71,7 +71,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap index a0c8fd1ef68e4..f1db6d7a8bfdc 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap @@ -16,7 +16,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -33,7 +33,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -50,7 +50,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__mixed_space_and_tab.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__mixed_space_and_tab.py.snap index 2ff76b7092a66..7ff04c9571b70 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__mixed_space_and_tab.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__mixed_space_and_tab.py.snap @@ -31,7 +31,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -64,7 +64,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -97,7 +97,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap index eea51abf01ce3..fdedcce512dd8 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap @@ -82,7 +82,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -164,7 +164,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Enabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@quote_style.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@quote_style.py.snap index 9bce249d23c2f..ed5c09022e14e 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@quote_style.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@quote_style.py.snap @@ -66,7 +66,7 @@ quote-style = Single line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -137,7 +137,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -208,7 +208,7 @@ quote-style = Preserve line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap index b7e43061b7da2..83b67689f467f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap @@ -49,7 +49,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -105,7 +105,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Ignore docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__assignment_split_value_first.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__assignment_split_value_first.py.snap index 6fca0005232fd..38e2e31a44354 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__assignment_split_value_first.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__assignment_split_value_first.py.snap @@ -234,7 +234,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Enabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@tab_width.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@tab_width.py.snap index 9f39a521a260a..ea85babc1bab7 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@tab_width.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@tab_width.py.snap @@ -24,7 +24,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -49,7 +49,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` @@ -77,7 +77,7 @@ quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect docstring-code = Disabled -docstring-code-line-width = 88 +docstring-code-line-width = "dynamic" preview = Disabled ``` diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 6b3e2abbc45a1..fe95418e69c43 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -34,7 +34,9 @@ use ruff_linter::settings::{ use ruff_linter::{ fs, warn_user, warn_user_once, warn_user_once_by_id, RuleSelector, RUFF_PKG_VERSION, }; -use ruff_python_formatter::{MagicTrailingComma, QuoteStyle}; +use ruff_python_formatter::{ + DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, QuoteStyle, +}; use crate::options::{ Flake8AnnotationsOptions, Flake8BanditOptions, Flake8BugbearOptions, Flake8BuiltinsOptions, @@ -189,6 +191,12 @@ impl Configuration { magic_trailing_comma: format .magic_trailing_comma .unwrap_or(format_defaults.magic_trailing_comma), + docstring_code_format: format + .docstring_code_format + .unwrap_or(format_defaults.docstring_code_format), + docstring_code_line_width: format + .docstring_code_line_width + .unwrap_or(format_defaults.docstring_code_line_width), }; let lint = self.lint; @@ -1020,6 +1028,8 @@ pub struct FormatConfiguration { pub quote_style: Option, pub magic_trailing_comma: Option, pub line_ending: Option, + pub docstring_code_format: Option, + pub docstring_code_line_width: Option, } impl FormatConfiguration { @@ -1046,6 +1056,14 @@ impl FormatConfiguration { } }), line_ending: options.line_ending, + docstring_code_format: options.docstring_code_format.map(|yes| { + if yes { + DocstringCode::Enabled + } else { + DocstringCode::Disabled + } + }), + docstring_code_line_width: options.docstring_code_line_length, }) } @@ -1059,6 +1077,10 @@ impl FormatConfiguration { quote_style: self.quote_style.or(other.quote_style), magic_trailing_comma: self.magic_trailing_comma.or(other.magic_trailing_comma), line_ending: self.line_ending.or(other.line_ending), + docstring_code_format: self.docstring_code_format.or(other.docstring_code_format), + docstring_code_line_width: self + .docstring_code_line_width + .or(other.docstring_code_line_width), } } } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index c0ce2c2e79f98..b032c2be3acf5 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -27,7 +27,7 @@ use ruff_linter::settings::types::{ }; use ruff_linter::{warn_user_once, RuleSelector}; use ruff_macros::{CombineOptions, OptionsMetadata}; -use ruff_python_formatter::QuoteStyle; +use ruff_python_formatter::{DocstringCodeLineWidth, QuoteStyle}; use crate::settings::LineEnding; @@ -2948,6 +2948,156 @@ pub struct FormatOptions { "# )] pub line_ending: Option, + + /// Whether to format code snippets in docstrings. + /// + /// When this is enabled, Python code examples within docstrings are + /// automatically reformatted. + /// + /// For example, when this is enabled, the following code: + /// + /// ```python + /// def f(x): + /// """ + /// Something about `f`. And an example in doctest format: + /// + /// >>> f( x ) + /// + /// Markdown is also supported: + /// + /// ```py + /// f( x ) + /// ``` + /// + /// As are reStructuredText literal blocks:: + /// + /// f( x ) + /// + /// + /// And reStructuredText code blocks: + /// + /// .. code-block:: python + /// + /// f( x ) + /// """ + /// pass + /// ``` + /// + /// ... will be reformatted (assuming the rest of the options are set to + /// their defaults) as: + /// + /// ```python + /// def f(x): + /// """ + /// Something about `f`. And an example in doctest format: + /// + /// >>> f(x) + /// + /// Markdown is also supported: + /// + /// ```py + /// f(x) + /// ``` + /// + /// As are reStructuredText literal blocks:: + /// + /// f(x) + /// + /// + /// And reStructuredText code blocks: + /// + /// .. code-block:: python + /// + /// f(x) + /// """ + /// pass + /// ``` + /// + /// If a code snippt in a docstring contains invalid Python code or if the + /// formatter would otherwise write invalid Python code, then the code + /// example is ignored by the formatter and kept as-is. + /// + /// Currently, doctest, Markdown, reStructuredText literal blocks, and + /// reStructuredText code blocks are all supported and automatically + /// recognized. In the case of unlabeled fenced code blocks in Markdown and + /// reStructuredText literal blocks, the contents are assumed to be Python + /// and reformatted. As with any other format, if the contents aren't valid + /// Python, then the block is left untouched automatically. + #[option( + default = "false", + value_type = "bool", + example = r#" + # Enable reformatting of code snippets in docstrings. + docstring-code-format = true + "# + )] + pub docstring_code_format: Option, + + /// Set the line length used when formatting code snippets in docstrings. + /// + /// This only has an effect when the `docstring-code-format` setting is + /// enabled. + /// + /// The default value for this setting is `"dynamic"`, which has the effect + /// of ensuring that any reformatted code examples in docstrings adhere to + /// the global line length configuration that is used for the surrounding + /// Python code. The point of this setting is that it takes the indentation + /// of the docstring into account when reformatting code examples. + /// + /// Alternatively, this can be set to a fixed integer, which will result + /// in the same line length limit being applied to all reformatted code + /// examples in docstrings. When set to a fixed integer, the indent of the + /// docstring is not taken into account. That is, this may result in lines + /// in the reformatted code example that exceed the globally configured + /// line length limit. + /// + /// For example, when this is set to `20` and `docstring-code-format` is + /// enabled, then this code: + /// + /// ```python + /// def f(x): + /// ''' + /// Something about `f`. And an example: + /// + /// .. code-block:: python + /// + /// foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear) + /// ''' + /// pass + /// ``` + /// + /// ... will be reformatted (assuming the rest of the options are set + /// to their defaults) as: + /// + /// ```python + /// def f(x): + /// """ + /// Something about `f`. And an example: + /// + /// .. code-block:: python + /// + /// ( + /// foo, + /// bar, + /// quux, + /// ) = this_is_a_long_line( + /// lion, + /// hippo, + /// lemur, + /// bear, + /// ) + /// """ + /// pass + /// ``` + #[option( + default = r#""dynamic""#, + value_type = r#"int | "dynamic""#, + example = r#" + # Format all docstring code snippets with a line length of 60. + docstring-code-line-length = 60 + "# + )] + pub docstring_code_line_length: Option, } #[cfg(test)] diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index 982732e487317..8ee030ea3b42a 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -5,7 +5,10 @@ use ruff_linter::settings::types::{FilePattern, FilePatternSet, SerializationFor use ruff_linter::settings::LinterSettings; use ruff_macros::CacheKey; use ruff_python_ast::PySourceType; -use ruff_python_formatter::{MagicTrailingComma, PreviewMode, PyFormatOptions, QuoteStyle}; +use ruff_python_formatter::{ + DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, PreviewMode, PyFormatOptions, + QuoteStyle, +}; use ruff_source_file::find_newline; use std::path::{Path, PathBuf}; @@ -124,6 +127,9 @@ pub struct FormatterSettings { pub magic_trailing_comma: MagicTrailingComma, pub line_ending: LineEnding, + + pub docstring_code_format: DocstringCode, + pub docstring_code_line_width: DocstringCodeLineWidth, } impl FormatterSettings { @@ -157,6 +163,8 @@ impl FormatterSettings { .with_preview(self.preview) .with_line_ending(line_ending) .with_line_width(self.line_width) + .with_docstring_code(self.docstring_code_format) + .with_docstring_code_line_width(self.docstring_code_line_width) } } @@ -173,6 +181,8 @@ impl Default for FormatterSettings { indent_width: default_options.indent_width(), quote_style: default_options.quote_style(), magic_trailing_comma: default_options.magic_trailing_comma(), + docstring_code_format: default_options.docstring_code(), + docstring_code_line_width: default_options.docstring_code_line_width(), } } } diff --git a/docs/configuration.md b/docs/configuration.md index 32d6b2eb130fc..ec1dd0fa577df 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -71,6 +71,20 @@ If left unspecified, Ruff's default configuration is equivalent to: # Like Black, automatically detect the appropriate line ending. line-ending = "auto" + + # Enable auto-formatting of code examples in docstrings. Markdown, + # reStructuredText code/literal blocks and doctests are all supported. + # + # This is currently disabled by default, but it is planned for this + # to be opt-out in the future. + docstring-code-format = false + + # Set the line length limit used when formatting code snippets in + # docstrings. + # + # This only has an effect when the `docstring-code-format` setting is + # enabled. + docstring-code-line-length = "dynamic" ``` === "ruff.toml" @@ -134,6 +148,20 @@ If left unspecified, Ruff's default configuration is equivalent to: # Like Black, automatically detect the appropriate line ending. line-ending = "auto" + + # Enable auto-formatting of code examples in docstrings. Markdown, + # reStructuredText code/literal blocks and doctests are all supported. + # + # This is currently disabled by default, but it is planned for this + # to be opt-out in the future. + docstring-code-format = false + + # Set the line length limit used when formatting code snippets in + # docstrings. + # + # This only has an effect when the `docstring-code-format` setting is + # enabled. + docstring-code-line-length = "dynamic" ``` As an example, the following would configure Ruff to: diff --git a/docs/formatter.md b/docs/formatter.md index 14d35e94a2dcd..8c8537946862d 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -103,10 +103,12 @@ Going forward, the Ruff Formatter will support Black's preview style under Ruff' ## Configuration The Ruff Formatter exposes a small set of configuration options, some of which are also supported -by Black (like line width), some of which are unique to Ruff (like quote and indentation style). +by Black (like line width), some of which are unique to Ruff (like quote, indentation style and +formatting code examples in docstrings). -For example, to configure the formatter to use single quotes, a line width of 100, and -tab indentation, add the following to your configuration file: +For example, to configure the formatter to use single quotes, format code +examples in docstrings, a line width of 100, and tab indentation, add the +following to your configuration file: === "pyproject.toml" @@ -117,6 +119,7 @@ tab indentation, add the following to your configuration file: [tool.ruff.format] quote-style = "single" indent-style = "tab" + docstring-code-format = true ``` === "ruff.toml" @@ -127,6 +130,7 @@ tab indentation, add the following to your configuration file: [format] quote-style = "single" indent-style = "tab" + docstring-code-format = true ``` @@ -137,6 +141,97 @@ Given the focus on Black compatibility (and unlike formatters like [YAPF](https: Ruff does not currently expose any configuration options to modify core formatting behavior outside of these trivia-related settings. +## Docstring formatting + +The Ruff formatter provides an opt-in feature for automatically formatting +Python code examples in docstrings. The Ruff formatter currently recognizes +code examples in the following formats: + +* The Python [doctest] format. +* CommonMark [fenced code blocks] with the following info strings: `python`, +`py`, `python3`, or `py3`. Fenced code blocks without an info string are +assumed to be Python code examples and also formatted. +* reStructuredText [literal blocks]. While literal blocks may contain things +other than Python, this is meant to reflect a long-standing convention in the +Python ecosystem where literal blocks often contain Python code. +* reStructuredText [`code-block` and `sourcecode` directives]. As with +Markdown, the language names recognized for Python are `python`, `py`, +`python3`, or `py3`. + +If a code example is recognized and treated as Python, the Ruff formatter will +automatically skip it if the code does not parse as valid Python or if the +reformatted code would produce an invalid Python program. + +Users may also configure the line length limit used for reformatting Python +code examples in docstrings. The default is a special value, `dynamic`, which +instructs the formatter to respect the line length limit setting for the +surrounding Python code. The `dynamic` setting ensures that even when code +examples are found inside indented docstrings, the line length limit configured +for the surrounding Python code will not be exceeded. Users may also configure +a fixed line length limit for code examples in docstrings. + +For example, this configuration shows how to enable docstring code formatting +with a fixed line length limit: + +=== "pyproject.toml" + + ```toml + [tool.ruff.format] + docstring-code-format = true + docstring-code-line-length = 20 + ``` + +=== "ruff.toml" + + ```toml + [format] + docstring-code-format = true + docstring-code-line-length = 20 + ``` + +With the above configuration, this code: + +```python +def f(x): + ''' + Something about `f`. And an example: + + .. code-block:: python + + foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear) + ''' + pass +``` + +... will be reformatted (assuming the rest of the options are set +to their defaults) as: + +```python +def f(x): + """ + Something about `f`. And an example: + + .. code-block:: python + + ( + foo, + bar, + quux, + ) = this_is_a_long_line( + lion, + hippo, + lemur, + bear, + ) + """ + pass +``` + +[doctest]: https://docs.python.org/3/library/doctest.html +[fenced code blocks]: https://spec.commonmark.org/0.30/#fenced-code-blocks +[literal blocks]: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks +[`code-block` and `sourcecode` directives]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block + ## Format suppression Like Black, Ruff supports `# fmt: on`, `# fmt: off`, and `# fmt: skip` pragma comments, which can diff --git a/ruff.schema.json b/ruff.schema.json index d7ff44db9d7b0..c382714b70616 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -747,6 +747,16 @@ } ] }, + "DocstringCodeLineWidth": { + "anyOf": [ + { + "$ref": "#/definitions/LineWidth" + }, + { + "type": "null" + } + ] + }, "Flake8AnnotationsOptions": { "type": "object", "properties": { @@ -1248,6 +1258,24 @@ "description": "Experimental: Configures how `ruff format` formats your code.\n\nPlease provide feedback in [this discussion](https://github.com/astral-sh/ruff/discussions/7310).", "type": "object", "properties": { + "docstring-code-format": { + "description": "Whether to format code snippets in docstrings.\n\nWhen this is enabled, Python code examples within docstrings are automatically reformatted.\n\nFor example, when this is enabled, the following code:\n\n```python def f(x): \"\"\" Something about `f`. And an example in doctest format:\n\n>>> f( x )\n\nMarkdown is also supported:\n\n```py f( x ) ```\n\nAs are reStructuredText literal blocks::\n\nf( x )\n\nAnd reStructuredText code blocks:\n\n.. code-block:: python\n\nf( x ) \"\"\" pass ```\n\n... will be reformatted (assuming the rest of the options are set to their defaults) as:\n\n```python def f(x): \"\"\" Something about `f`. And an example in doctest format:\n\n>>> f(x)\n\nMarkdown is also supported:\n\n```py f(x) ```\n\nAs are reStructuredText literal blocks::\n\nf(x)\n\nAnd reStructuredText code blocks:\n\n.. code-block:: python\n\nf(x) \"\"\" pass ```\n\nIf a code snippt in a docstring contains invalid Python code or if the formatter would otherwise write invalid Python code, then the code example is ignored by the formatter and kept as-is.\n\nCurrently, doctest, Markdown, reStructuredText literal blocks, and reStructuredText code blocks are all supported and automatically recognized. In the case of unlabeled fenced code blocks in Markdown and reStructuredText literal blocks, the contents are assumed to be Python and reformatted. As with any other format, if the contents aren't valid Python, then the block is left untouched automatically.", + "type": [ + "boolean", + "null" + ] + }, + "docstring-code-line-length": { + "description": "Set the line length used when formatting code snippets in docstrings.\n\nThis only has an effect when the `docstring-code-format` setting is enabled.\n\nThe default value for this setting is `\"dynamic\"`, which has the effect of ensuring that any reformatted code examples in docstrings adhere to the global line length configuration that is used for the surrounding Python code. The point of this setting is that it takes the indentation of the docstring into account when reformatting code examples.\n\nAlternatively, this can be set to a fixed integer, which will result in the same line length limit being applied to all reformatted code examples in docstrings. When set to a fixed integer, the indent of the docstring is not taken into account. That is, this may result in lines in the reformatted code example that exceed the globally configured line length limit.\n\nFor example, when this is set to `20` and `docstring-code-format` is enabled, then this code:\n\n```python def f(x): ''' Something about `f`. And an example:\n\n.. code-block:: python\n\nfoo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear) ''' pass ```\n\n... will be reformatted (assuming the rest of the options are set to their defaults) as:\n\n```python def f(x): \"\"\" Something about `f`. And an example:\n\n.. code-block:: python\n\n( foo, bar, quux, ) = this_is_a_long_line( lion, hippo, lemur, bear, ) \"\"\" pass ```", + "anyOf": [ + { + "$ref": "#/definitions/DocstringCodeLineWidth" + }, + { + "type": "null" + } + ] + }, "exclude": { "description": "A list of file patterns to exclude from formatting in addition to the files excluded globally (see [`exclude`](#exclude), and [`extend-exclude`](#extend-exclude)).\n\nExclusions are based on globs, and can be either:\n\n- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ). - Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py` (to exclude any Python files in `directory`). Note that these paths are relative to the project root (e.g., the directory containing your `pyproject.toml`).\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", "type": [ @@ -1652,6 +1680,12 @@ "maximum": 320.0, "minimum": 1.0 }, + "LineWidth": { + "description": "The maximum visual width to which the formatter should try to limit a line.", + "type": "integer", + "format": "uint16", + "minimum": 1.0 + }, "LintOptions": { "description": "Experimental section to configure Ruff's linting. This new section will eventually replace the top-level linting options.\n\nOptions specified in the `lint` section take precedence over the top-level settings.", "type": "object",