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.
110 lines
3.6 KiB
Rust
110 lines
3.6 KiB
Rust
#![allow(clippy::print_stdout)]
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use anyhow::{Context, Result};
|
|
use clap::{Parser, ValueEnum, command};
|
|
|
|
use ruff_formatter::SourceCode;
|
|
use ruff_python_ast::{PySourceType, PythonVersion};
|
|
use ruff_python_parser::{ParseOptions, parse};
|
|
use ruff_python_trivia::CommentRanges;
|
|
use ruff_text_size::Ranged;
|
|
|
|
use crate::comments::collect_comments;
|
|
use crate::{MagicTrailingComma, PreviewMode, PyFormatOptions, format_module_ast};
|
|
|
|
#[derive(ValueEnum, Clone, Debug)]
|
|
pub enum Emit {
|
|
/// Write back to the original files
|
|
Files,
|
|
/// Write to stdout
|
|
Stdout,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[command(author, version, about, long_about = None)]
|
|
#[expect(clippy::struct_excessive_bools)] // It's only the dev cli anyways
|
|
pub struct Cli {
|
|
/// Python files to format. If there are none, stdin will be used. `-` as stdin is not supported
|
|
pub files: Vec<PathBuf>,
|
|
#[clap(long)]
|
|
pub emit: Option<Emit>,
|
|
/// Run in 'check' mode. Exits with 0 if input is formatted correctly. Exits with 1 and prints
|
|
/// a diff if formatting is required.
|
|
#[clap(long)]
|
|
pub check: bool,
|
|
#[clap(long)]
|
|
pub preview: bool,
|
|
#[clap(long)]
|
|
pub print_ir: bool,
|
|
#[clap(long)]
|
|
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).with_target_version(cli.target_version),
|
|
)
|
|
.context("Syntax error in input")?;
|
|
|
|
let options = PyFormatOptions::from_extension(source_path)
|
|
.with_preview(if cli.preview {
|
|
PreviewMode::Enabled
|
|
} else {
|
|
PreviewMode::Disabled
|
|
})
|
|
.with_magic_trailing_comma(if cli.skip_magic_trailing_comma {
|
|
MagicTrailingComma::Ignore
|
|
} else {
|
|
MagicTrailingComma::Respect
|
|
})
|
|
.with_target_version(cli.target_version);
|
|
|
|
let source_code = SourceCode::new(source);
|
|
let comment_ranges = CommentRanges::from(parsed.tokens());
|
|
let formatted = format_module_ast(&parsed, &comment_ranges, source, options)
|
|
.context("Failed to format node")?;
|
|
if cli.print_ir {
|
|
println!("{}", formatted.document().display(source_code));
|
|
}
|
|
if cli.print_comments {
|
|
// Print preceding, following and enclosing nodes
|
|
let decorated_comments = collect_comments(parsed.syntax(), source_code, &comment_ranges);
|
|
if !decorated_comments.is_empty() {
|
|
println!("# Comment decoration: Range, Preceding, Following, Enclosing, Comment");
|
|
}
|
|
for comment in decorated_comments {
|
|
println!(
|
|
"{:?}, {:?}, {:?}, {:?}, {:?}",
|
|
comment.slice().range(),
|
|
comment
|
|
.preceding_node()
|
|
.map(|node| (node.kind(), node.range())),
|
|
comment
|
|
.following_node()
|
|
.map(|node| (node.kind(), node.range())),
|
|
(
|
|
comment.enclosing_node().kind(),
|
|
comment.enclosing_node().range()
|
|
),
|
|
comment.slice().text(source_code),
|
|
);
|
|
}
|
|
println!("{:#?}", formatted.context().comments().debug(source_code));
|
|
}
|
|
Ok(formatted
|
|
.print()
|
|
.context("Failed to print the formatter IR")?
|
|
.as_code()
|
|
.to_string())
|
|
}
|