Remove parentheses around multiple exception types on Python 3.14+ (#20768)

Summary
--

This PR implements the black preview style from
https://github.com/psf/black/pull/4720. As of Python 3.14, you're
allowed to omit the parentheses around groups of exceptions, as long as
there's no `as` binding:

**3.13**

```pycon
Python 3.13.4 (main, Jun  4 2025, 17:37:06) [Clang 20.1.4 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> try: ...
... except (Exception, BaseException): ...
...
Ellipsis
>>> try: ...
... except Exception, BaseException: ...
...
  File "<python-input-1>", line 2
    except Exception, BaseException: ...
           ^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: multiple exception types must be parenthesized
```

**3.14**

```pycon
Python 3.14.0rc2 (main, Sep  2 2025, 14:20:56) [Clang 20.1.4 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> try: ...
... except Exception, BaseException: ...
...
Ellipsis
>>> try: ...
... except (Exception, BaseException): ...
...
Ellipsis
>>> try: ...
... except Exception, BaseException as e: ...
...
  File "<python-input-2>", line 2
    except Exception, BaseException as e: ...
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: multiple exception types must be parenthesized when using 'as'
```

I think this ended up being pretty straightforward, at least once Micha
showed me where to start :)

Test Plan
--

New tests

At first I thought we were deviating from black in how we handle
comments within the exception type tuple, but I think this applies to
how we format all tuples, not specifically with the new preview style.
This commit is contained in:
Brent Westbrook
2025-10-14 11:17:45 -04:00
committed by GitHub
parent 1ed9b215b9
commit 591e9bbccb
8 changed files with 497 additions and 450 deletions

View File

@@ -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<String> {
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());