diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.options.json new file mode 100644 index 0000000000..f1fa9100c1 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.options.json @@ -0,0 +1,8 @@ +[ + { + "target_version": "3.13" + }, + { + "target_version": "3.14" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py index a5cdf3ec9f..2e4a7634ef 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py @@ -166,3 +166,40 @@ else: finally: pass + + +try: + pass +# These parens can be removed on 3.14+ but not earlier +except (BaseException, Exception, ValueError): + pass +# But black won't remove these parentheses +except (ZeroDivisionError,): + pass +except ( # We wrap these and preserve the parens + BaseException, Exception, ValueError): + pass +except ( + BaseException, + # Same with this comment + Exception, + ValueError +): + pass + +try: + pass +# They can also be omitted for `except*` +except* (BaseException, Exception, ValueError): + pass + +# But parentheses are still required in the presence of an `as` binding +try: + pass +except (BaseException, Exception, ValueError) as e: + pass + +try: + pass +except* (BaseException, Exception, ValueError) as e: + pass diff --git a/crates/ruff_python_formatter/src/cli.rs b/crates/ruff_python_formatter/src/cli.rs index 60f7cd413c..96da64e86e 100644 --- a/crates/ruff_python_formatter/src/cli.rs +++ b/crates/ruff_python_formatter/src/cli.rs @@ -6,7 +6,7 @@ use anyhow::{Context, Result}; use clap::{Parser, ValueEnum, command}; use ruff_formatter::SourceCode; -use ruff_python_ast::PySourceType; +use ruff_python_ast::{PySourceType, PythonVersion}; use ruff_python_parser::{ParseOptions, parse}; use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; @@ -42,13 +42,19 @@ pub struct Cli { pub print_comments: bool, #[clap(long, short = 'C')] pub skip_magic_trailing_comma: bool, + #[clap(long)] + pub target_version: PythonVersion, } pub fn format_and_debug_print(source: &str, cli: &Cli, source_path: &Path) -> Result { let source_type = PySourceType::from(source_path); // Parse the AST. - let parsed = parse(source, ParseOptions::from(source_type)).context("Syntax error in input")?; + let parsed = parse( + source, + ParseOptions::from(source_type).with_target_version(cli.target_version), + ) + .context("Syntax error in input")?; let options = PyFormatOptions::from_extension(source_path) .with_preview(if cli.preview { @@ -60,7 +66,8 @@ pub fn format_and_debug_print(source: &str, cli: &Cli, source_path: &Path) -> Re MagicTrailingComma::Ignore } else { MagicTrailingComma::Respect - }); + }) + .with_target_version(cli.target_version); let source_code = SourceCode::new(source); let comment_ranges = CommentRanges::from(parsed.tokens()); diff --git a/crates/ruff_python_formatter/src/expression/expr_tuple.rs b/crates/ruff_python_formatter/src/expression/expr_tuple.rs index 6d95048673..8eba63a2ac 100644 --- a/crates/ruff_python_formatter/src/expression/expr_tuple.rs +++ b/crates/ruff_python_formatter/src/expression/expr_tuple.rs @@ -39,8 +39,8 @@ pub enum TupleParentheses { /// /// ```python /// return len(self.nodeseeeeeeeee), sum( - // len(node.parents) for node in self.node_map.values() - // ) + /// len(node.parents) for node in self.node_map.values() + /// ) /// ``` OptionalParentheses, diff --git a/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs index 4f2f93a3e9..aefaaec182 100644 --- a/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs +++ b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs @@ -1,10 +1,12 @@ use ruff_formatter::FormatRuleWithOptions; use ruff_formatter::write; -use ruff_python_ast::ExceptHandlerExceptHandler; +use ruff_python_ast::{ExceptHandlerExceptHandler, Expr, PythonVersion}; +use crate::expression::expr_tuple::TupleParentheses; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; +use crate::preview::is_remove_parens_around_except_types_enabled; use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; use crate::statement::suite::SuiteKind; @@ -57,7 +59,7 @@ impl FormatNodeRule for FormatExceptHandlerExceptHan clause_header( ClauseHeader::ExceptHandler(item), dangling_comments, - &format_with(|f| { + &format_with(|f: &mut PyFormatter| { write!( f, [ @@ -69,21 +71,50 @@ impl FormatNodeRule for FormatExceptHandlerExceptHan ] )?; - if let Some(type_) = type_ { - write!( - f, - [ - space(), - maybe_parenthesize_expression( - type_, - item, - Parenthesize::IfBreaks + match type_.as_deref() { + // For tuples of exception types without an `as` name and on 3.14+, the + // parentheses are optional. + // + // ```py + // try: + // ... + // except BaseException, Exception: # Ok + // ... + // ``` + Some(Expr::Tuple(tuple)) + if f.options().target_version() >= PythonVersion::PY314 + && is_remove_parens_around_except_types_enabled( + f.context(), ) - ] - )?; - if let Some(name) = name { - write!(f, [space(), token("as"), space(), name.format()])?; + && name.is_none() => + { + write!( + f, + [ + space(), + tuple + .format() + .with_options(TupleParentheses::NeverPreserve) + ] + )?; } + Some(type_) => { + write!( + f, + [ + space(), + maybe_parenthesize_expression( + type_, + item, + Parenthesize::IfBreaks + ) + ] + )?; + if let Some(name) = name { + write!(f, [space(), token("as"), space(), name.format()])?; + } + } + _ => {} } Ok(()) diff --git a/crates/ruff_python_formatter/src/preview.rs b/crates/ruff_python_formatter/src/preview.rs index ee9b378cb8..b6479ab1b4 100644 --- a/crates/ruff_python_formatter/src/preview.rs +++ b/crates/ruff_python_formatter/src/preview.rs @@ -27,3 +27,12 @@ pub(crate) const fn is_blank_line_before_decorated_class_in_stub_enabled( ) -> bool { context.is_preview() } + +/// Returns `true` if the +/// [`remove_parens_around_except_types`](https://github.com/astral-sh/ruff/pull/20768) preview +/// style is enabled. +pub(crate) const fn is_remove_parens_around_except_types_enabled( + context: &PyFormatContext, +) -> bool { + context.is_preview() +} diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__remove_except_types_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__remove_except_types_parens.py.snap deleted file mode 100644 index 1562391b41..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__remove_except_types_parens.py.snap +++ /dev/null @@ -1,427 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/remove_except_types_parens.py ---- -## Input - -```python -# SEE PEP 758 FOR MORE DETAILS -# remains unchanged -try: - pass -except: - pass - -# remains unchanged -try: - pass -except ValueError: - pass - -try: - pass -except* ValueError: - pass - -# parenthesis are removed -try: - pass -except (ValueError): - pass - -try: - pass -except* (ValueError): - pass - -# parenthesis are removed -try: - pass -except (ValueError) as e: - pass - -try: - pass -except* (ValueError) as e: - pass - -# remains unchanged -try: - pass -except (ValueError,): - pass - -try: - pass -except* (ValueError,): - pass - -# remains unchanged -try: - pass -except (ValueError,) as e: - pass - -try: - pass -except* (ValueError,) as e: - pass - -# remains unchanged -try: - pass -except ValueError, TypeError, KeyboardInterrupt: - pass - -try: - pass -except* ValueError, TypeError, KeyboardInterrupt: - pass - -# parenthesis are removed -try: - pass -except (ValueError, TypeError, KeyboardInterrupt): - pass - -try: - pass -except* (ValueError, TypeError, KeyboardInterrupt): - pass - -# parenthesis are not removed -try: - pass -except (ValueError, TypeError, KeyboardInterrupt) as e: - pass - -try: - pass -except* (ValueError, TypeError, KeyboardInterrupt) as e: - pass - -# parenthesis are removed -try: - pass -except (ValueError if True else TypeError): - pass - -try: - pass -except* (ValueError if True else TypeError): - pass - -# inner except: parenthesis are removed -# outer except: parenthsis are not removed -try: - try: - pass - except (TypeError, KeyboardInterrupt): - pass -except (ValueError,): - pass - -try: - try: - pass - except* (TypeError, KeyboardInterrupt): - pass -except* (ValueError,): - pass -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -74,12 +74,12 @@ - # parenthesis are removed - try: - pass --except ValueError, TypeError, KeyboardInterrupt: -+except (ValueError, TypeError, KeyboardInterrupt): - pass - - try: - pass --except* ValueError, TypeError, KeyboardInterrupt: -+except* (ValueError, TypeError, KeyboardInterrupt): - pass - - # parenthesis are not removed -@@ -109,7 +109,7 @@ - try: - try: - pass -- except TypeError, KeyboardInterrupt: -+ except (TypeError, KeyboardInterrupt): - pass - except (ValueError,): - pass -@@ -117,7 +117,7 @@ - try: - try: - pass -- except* TypeError, KeyboardInterrupt: -+ except* (TypeError, KeyboardInterrupt): - pass - except* (ValueError,): - pass -``` - -## Ruff Output - -```python -# SEE PEP 758 FOR MORE DETAILS -# remains unchanged -try: - pass -except: - pass - -# remains unchanged -try: - pass -except ValueError: - pass - -try: - pass -except* ValueError: - pass - -# parenthesis are removed -try: - pass -except ValueError: - pass - -try: - pass -except* ValueError: - pass - -# parenthesis are removed -try: - pass -except ValueError as e: - pass - -try: - pass -except* ValueError as e: - pass - -# remains unchanged -try: - pass -except (ValueError,): - pass - -try: - pass -except* (ValueError,): - pass - -# remains unchanged -try: - pass -except (ValueError,) as e: - pass - -try: - pass -except* (ValueError,) as e: - pass - -# remains unchanged -try: - pass -except ValueError, TypeError, KeyboardInterrupt: - pass - -try: - pass -except* ValueError, TypeError, KeyboardInterrupt: - pass - -# parenthesis are removed -try: - pass -except (ValueError, TypeError, KeyboardInterrupt): - pass - -try: - pass -except* (ValueError, TypeError, KeyboardInterrupt): - pass - -# parenthesis are not removed -try: - pass -except (ValueError, TypeError, KeyboardInterrupt) as e: - pass - -try: - pass -except* (ValueError, TypeError, KeyboardInterrupt) as e: - pass - -# parenthesis are removed -try: - pass -except ValueError if True else TypeError: - pass - -try: - pass -except* ValueError if True else TypeError: - pass - -# inner except: parenthesis are removed -# outer except: parenthsis are not removed -try: - try: - pass - except (TypeError, KeyboardInterrupt): - pass -except (ValueError,): - pass - -try: - try: - pass - except* (TypeError, KeyboardInterrupt): - pass -except* (ValueError,): - pass -``` - -## Black Output - -```python -# SEE PEP 758 FOR MORE DETAILS -# remains unchanged -try: - pass -except: - pass - -# remains unchanged -try: - pass -except ValueError: - pass - -try: - pass -except* ValueError: - pass - -# parenthesis are removed -try: - pass -except ValueError: - pass - -try: - pass -except* ValueError: - pass - -# parenthesis are removed -try: - pass -except ValueError as e: - pass - -try: - pass -except* ValueError as e: - pass - -# remains unchanged -try: - pass -except (ValueError,): - pass - -try: - pass -except* (ValueError,): - pass - -# remains unchanged -try: - pass -except (ValueError,) as e: - pass - -try: - pass -except* (ValueError,) as e: - pass - -# remains unchanged -try: - pass -except ValueError, TypeError, KeyboardInterrupt: - pass - -try: - pass -except* ValueError, TypeError, KeyboardInterrupt: - pass - -# parenthesis are removed -try: - pass -except ValueError, TypeError, KeyboardInterrupt: - pass - -try: - pass -except* ValueError, TypeError, KeyboardInterrupt: - pass - -# parenthesis are not removed -try: - pass -except (ValueError, TypeError, KeyboardInterrupt) as e: - pass - -try: - pass -except* (ValueError, TypeError, KeyboardInterrupt) as e: - pass - -# parenthesis are removed -try: - pass -except ValueError if True else TypeError: - pass - -try: - pass -except* ValueError if True else TypeError: - pass - -# inner except: parenthesis are removed -# outer except: parenthsis are not removed -try: - try: - pass - except TypeError, KeyboardInterrupt: - pass -except (ValueError,): - pass - -try: - try: - pass - except* TypeError, KeyboardInterrupt: - pass -except* (ValueError,): - pass -``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap index fc6fcf4406..0dd13e8948 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__try.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py -snapshot_kind: text --- ## Input ```python @@ -173,9 +172,61 @@ else: finally: pass + + +try: + pass +# These parens can be removed on 3.14+ but not earlier +except (BaseException, Exception, ValueError): + pass +# But black won't remove these parentheses +except (ZeroDivisionError,): + pass +except ( # We wrap these and preserve the parens + BaseException, Exception, ValueError): + pass +except ( + BaseException, + # Same with this comment + Exception, + ValueError +): + pass + +try: + pass +# They can also be omitted for `except*` +except* (BaseException, Exception, ValueError): + pass + +# But parentheses are still required in the presence of an `as` binding +try: + pass +except (BaseException, Exception, ValueError) as e: + pass + +try: + pass +except* (BaseException, Exception, ValueError) as e: + pass +``` + +## Outputs +### Output 1 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.13 +source_type = Python ``` -## Output ```python try: pass @@ -364,10 +415,50 @@ else: finally: pass + + +try: + pass +# These parens can be removed on 3.14+ but not earlier +except (BaseException, Exception, ValueError): + pass +# But black won't remove these parentheses +except (ZeroDivisionError,): + pass +except ( # We wrap these and preserve the parens + BaseException, + Exception, + ValueError, +): + pass +except ( + BaseException, + # Same with this comment + Exception, + ValueError, +): + pass + +try: + pass +# They can also be omitted for `except*` +except* (BaseException, Exception, ValueError): + pass + +# But parentheses are still required in the presence of an `as` binding +try: + pass +except (BaseException, Exception, ValueError) as e: + pass + +try: + pass +except* (BaseException, Exception, ValueError) as e: + pass ``` -## Preview changes +#### Preview changes ```diff --- Stable +++ Preview @@ -392,3 +483,294 @@ finally: def f(): ``` + + +### Output 2 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.14 +source_type = Python +``` + +```python +try: + pass +except: + pass + +try: + pass +except KeyError: # should remove brackets and be a single line + pass + + +try: # try + pass + # end of body +# before except +except (Exception, ValueError) as exc: # except line + pass +# before except 2 +except KeyError as key: # except line 2 + pass + # in body 2 +# before else +else: + pass +# before finally +finally: + pass + + +# with line breaks +try: # try + pass + # end of body + +# before except +except (Exception, ValueError) as exc: # except line + pass + +# before except 2 +except KeyError as key: # except line 2 + pass + # in body 2 + +# before else +else: + pass + +# before finally +finally: + pass + + +# with line breaks +try: + pass + +except: + pass + + +try: + pass +except ( + Exception, + Exception, + Exception, + Exception, + Exception, + Exception, + Exception, +) as exc: # splits exception over multiple lines + pass + + +try: + pass +except: + a = 10 # trailing comment1 + b = 11 # trailing comment2 + + +# try/except*, mostly the same as try +try: # try + pass + # end of body +# before except +except* (Exception, ValueError) as exc: # except line + pass +# before except 2 +except* KeyError as key: # except line 2 + pass + # in body 2 +# before else +else: + pass +# before finally +finally: + pass + +# try and try star are statements with body +# Minimized from https://github.com/python/cpython/blob/99b00efd5edfd5b26bf9e2a35cbfc96277fdcbb1/Lib/getpass.py#L68-L91 +try: + try: + pass + finally: + print(1) # issue7208 +except A: + pass + +try: + f() # end-of-line last comment +except RuntimeError: + raise + +try: + + def f(): + pass + # a +except: + + def f(): + pass + # b +else: + + def f(): + pass + # c +finally: + + def f(): + pass + # d + + +try: + pass # a +except ZeroDivisionError: + pass # b +except: + pass # c +else: + pass # d +finally: + pass # e + +try: # 1 preceding: any, following: first in body, enclosing: try + print(1) # 2 preceding: last in body, following: fist in alt body, enclosing: try +except ( + ZeroDivisionError +): # 3 preceding: test, following: fist in alt body, enclosing: try + print(2) # 4 preceding: last in body, following: fist in alt body, enclosing: exc +except: # 5 preceding: last in body, following: fist in alt body, enclosing: try + print(2) # 6 preceding: last in body, following: fist in alt body, enclosing: exc +else: # 7 preceding: last in body, following: fist in alt body, enclosing: exc + print(3) # 8 preceding: last in body, following: fist in alt body, enclosing: try +finally: # 9 preceding: last in body, following: fist in alt body, enclosing: try + print(3) # 10 preceding: last in body, following: any, enclosing: try + +try: + pass +except ( + ZeroDivisionError + # comment +): + pass + + +try: + pass + +finally: + pass + + +try: + pass + +except ZeroDivisonError: + pass + +else: + pass + +finally: + pass + + +try: + pass +# These parens can be removed on 3.14+ but not earlier +except (BaseException, Exception, ValueError): + pass +# But black won't remove these parentheses +except (ZeroDivisionError,): + pass +except ( # We wrap these and preserve the parens + BaseException, + Exception, + ValueError, +): + pass +except ( + BaseException, + # Same with this comment + Exception, + ValueError, +): + pass + +try: + pass +# They can also be omitted for `except*` +except* (BaseException, Exception, ValueError): + pass + +# But parentheses are still required in the presence of an `as` binding +try: + pass +except (BaseException, Exception, ValueError) as e: + pass + +try: + pass +except* (BaseException, Exception, ValueError) as e: + pass +``` + + +#### Preview changes +```diff +--- Stable ++++ Preview +@@ -117,16 +117,19 @@ + def f(): + pass + # a ++ + except: + + def f(): + pass + # b ++ + else: + + def f(): + pass + # c ++ + finally: + + def f(): +@@ -190,7 +193,7 @@ + try: + pass + # These parens can be removed on 3.14+ but not earlier +-except (BaseException, Exception, ValueError): ++except BaseException, Exception, ValueError: + pass + # But black won't remove these parentheses + except (ZeroDivisionError,): +@@ -212,7 +215,7 @@ + try: + pass + # They can also be omitted for `except*` +-except* (BaseException, Exception, ValueError): ++except* BaseException, Exception, ValueError: + pass + + # But parentheses are still required in the presence of an `as` binding +```