diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/match.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/match.py index 421055cec9..fbe20f7e83 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/match.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/match.py @@ -264,6 +264,7 @@ match foo: y = 1 + match foo: case [1, 2, *rest]: pass @@ -399,3 +400,61 @@ match foo: b, }: pass + + +match pattern_match_class: + case Point2D( + # own line + ): + ... + + case ( + Point2D + # own line + () + ): + ... + + case Point2D( # end of line line + ): + ... + + case Point2D( # end of line + 0, 0 + ): + ... + + case Point2D(0, 0): + ... + + case Point2D( + ( # end of line + # own line + 0 + ), 0): + ... + + case Point3D(x=0, y=0, z=000000000000000000000000000000000000000000000000000000000000000000000000000000000): + ... + + case Bar(0, a=None, b="hello"): + ... + + case FooBar(# leading +# leading + # leading + # leading + 0 # trailing +# trailing + # trailing + # trailing + ): + ... + + case A( + b # b + = # c + 2 # d + # e + ): + pass diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index eb7f189815..44f8aa7500 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -180,15 +180,7 @@ fn handle_enclosed_comment<'a>( AnyNodeRef::Comprehension(comprehension) => { handle_comprehension_comment(comment, comprehension, locator) } - AnyNodeRef::PatternMatchSequence(pattern_match_sequence) => { - if SequenceType::from_pattern(pattern_match_sequence, locator.contents()) - .is_parenthesized() - { - handle_bracketed_end_of_line_comment(comment, locator) - } else { - CommentPlacement::Default(comment) - } - } + AnyNodeRef::ExprAttribute(attribute) => { handle_attribute_comment(comment, attribute, locator) } @@ -219,6 +211,18 @@ fn handle_enclosed_comment<'a>( handle_module_level_own_line_comment_before_class_or_function_comment(comment, locator) } AnyNodeRef::WithItem(_) => handle_with_item_comment(comment, locator), + AnyNodeRef::PatternMatchSequence(pattern_match_sequence) => { + if SequenceType::from_pattern(pattern_match_sequence, locator.contents()) + .is_parenthesized() + { + handle_bracketed_end_of_line_comment(comment, locator) + } else { + CommentPlacement::Default(comment) + } + } + AnyNodeRef::PatternMatchClass(class) => { + handle_pattern_match_class_comment(comment, class, locator) + } AnyNodeRef::PatternMatchAs(_) => handle_pattern_match_as_comment(comment, locator), AnyNodeRef::PatternMatchStar(_) => handle_pattern_match_star_comment(comment), AnyNodeRef::PatternMatchMapping(pattern) => { @@ -1229,6 +1233,77 @@ fn handle_with_item_comment<'a>( } } +/// Handles trailing comments after the `as` keyword of a pattern match item: +/// +/// ```python +/// case ( +/// Pattern +/// # dangling +/// ( # dangling +/// # dangling +/// ) +/// ): ... +/// ``` +fn handle_pattern_match_class_comment<'a>( + comment: DecoratedComment<'a>, + class: &'a ast::PatternMatchClass, + locator: &Locator, +) -> CommentPlacement<'a> { + // Find the open parentheses on the arguments. + let Some(left_paren) = SimpleTokenizer::starts_at(class.cls.end(), locator.contents()) + .skip_trivia() + .find(|token| token.kind == SimpleTokenKind::LParen) + else { + return CommentPlacement::Default(comment); + }; + + // If the comment appears before the open parenthesis, it's dangling: + // ```python + // case ( + // Pattern + // # dangling + // (...) + // ): ... + // ``` + if comment.end() < left_paren.start() { + return CommentPlacement::dangling(comment.enclosing_node(), comment); + } + + let Some(first_item) = class + .patterns + .first() + .map(Ranged::start) + .or_else(|| class.kwd_attrs.first().map(Ranged::start)) + else { + // If there are no items, then the comment must be dangling: + // ```python + // case ( + // Pattern( + // # dangling + // ) + // ): ... + // ``` + return CommentPlacement::dangling(comment.enclosing_node(), comment); + }; + + // If the comment appears before the first item or its parentheses, then it's dangling: + // ```python + // case ( + // Pattern( # dangling + // 0, + // 0, + // ) + // ): ... + // ``` + if comment.line_position().is_end_of_line() { + if comment.end() < first_item { + return CommentPlacement::dangling(comment.enclosing_node(), comment); + } + } + + CommentPlacement::Default(comment) +} + /// Handles trailing comments after the `as` keyword of a pattern match item: /// /// ```python diff --git a/crates/ruff_python_formatter/src/pattern/pattern_match_class.rs b/crates/ruff_python_formatter/src/pattern/pattern_match_class.rs index 84d11384e0..2cbe058ba6 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_match_class.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_match_class.rs @@ -1,23 +1,97 @@ -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::comments::{dangling_comments, SourceComment}; +use ruff_formatter::write; use ruff_python_ast::node::AnyNodeRef; -use ruff_python_ast::PatternMatchClass; +use ruff_python_ast::{Pattern, PatternMatchClass, Ranged}; +use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; +use ruff_text_size::{TextRange, TextSize}; -use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; +use crate::expression::parentheses::{ + empty_parenthesized, parenthesized, NeedsParentheses, OptionalParentheses, Parentheses, +}; use crate::prelude::*; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; #[derive(Default)] pub struct FormatPatternMatchClass; impl FormatNodeRule for FormatPatternMatchClass { fn fmt_fields(&self, item: &PatternMatchClass, f: &mut PyFormatter) -> FormatResult<()> { - write!( - f, - [not_yet_implemented_custom_text( - "NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0)", - item - )] - ) + let PatternMatchClass { + range, + cls, + patterns, + kwd_attrs, + kwd_patterns, + } = item; + + let comments = f.context().comments().clone(); + let dangling = comments.dangling(item); + + // Identify the dangling comments before and after the open parenthesis. + let (before_parenthesis, after_parenthesis) = if let Some(left_paren) = + SimpleTokenizer::starts_at(cls.end(), f.context().source()) + .find(|token| token.kind() == SimpleTokenKind::LParen) + { + dangling + .split_at(dangling.partition_point(|comment| comment.start() < left_paren.start())) + } else { + (dangling, [].as_slice()) + }; + + write!(f, [cls.format(), dangling_comments(before_parenthesis)])?; + + match (patterns.as_slice(), kwd_attrs.as_slice()) { + ([], []) => { + // No patterns; render parentheses with any dangling comments. + write!(f, [empty_parenthesized("(", after_parenthesis, ")")]) + } + ([pattern], []) => { + // A single pattern. We need to take care not to re-parenthesize it, since our standard + // parenthesis detection will false-positive here. + let parentheses = if is_single_argument_parenthesized( + pattern, + item.end(), + f.context().source(), + ) { + Parentheses::Always + } else { + Parentheses::Never + }; + write!( + f, + [ + parenthesized("(", &pattern.format().with_options(parentheses), ")") + .with_dangling_comments(after_parenthesis) + ] + ) + } + _ => { + // Multiple patterns: standard logic. + let items = format_with(|f| { + let mut join = f.join_comma_separated(range.end()); + join.nodes(patterns.iter()); + for (key, value) in kwd_attrs.iter().zip(kwd_patterns.iter()) { + join.entry( + key, + &format_with(|f| write!(f, [key.format(), text("="), value.format()])), + ); + } + join.finish() + }); + write!( + f, + [parenthesized("(", &group(&items), ")") + .with_dangling_comments(after_parenthesis)] + ) + } + } + } + + fn fmt_dangling_comments( + &self, + _dangling_comments: &[SourceComment], + _f: &mut PyFormatter, + ) -> FormatResult<()> { + Ok(()) } } @@ -25,8 +99,56 @@ impl NeedsParentheses for PatternMatchClass { fn needs_parentheses( &self, _parent: AnyNodeRef, - _context: &PyFormatContext, + context: &PyFormatContext, ) -> OptionalParentheses { + // If there are any comments outside of the class parentheses, break: + // ```python + // case ( + // Pattern + // # dangling + // (...) + // ): ... + // ``` + let dangling = context.comments().dangling(self); + if !dangling.is_empty() { + if let Some(left_paren) = SimpleTokenizer::starts_at(self.cls.end(), context.source()) + .find(|token| token.kind() == SimpleTokenKind::LParen) + { + if dangling + .iter() + .any(|comment| comment.start() < left_paren.start()) + { + return OptionalParentheses::Multiline; + }; + } + } OptionalParentheses::Never } } + +/// Returns `true` if the pattern (which is the only argument to a [`PatternMatchClass`]) is +/// parenthesized. Used to avoid falsely assuming that `x` is parenthesized in cases like: +/// ```python +/// case Point2D(x): ... +/// ``` +fn is_single_argument_parenthesized(pattern: &Pattern, call_end: TextSize, source: &str) -> bool { + let mut has_seen_r_paren = false; + for token in SimpleTokenizer::new(source, TextRange::new(pattern.end(), call_end)).skip_trivia() + { + match token.kind() { + SimpleTokenKind::RParen => { + if has_seen_r_paren { + return true; + } + has_seen_r_paren = true; + } + // Skip over any trailing comma + SimpleTokenKind::Comma => continue, + _ => { + // Passed the arguments + break; + } + } + } + false +} diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_complex.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_complex.py.snap index 1c7fe97abe..3a98fded6c 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_complex.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_complex.py.snap @@ -156,15 +156,6 @@ match x: ```diff --- Black +++ Ruff -@@ -6,7 +6,7 @@ - y = 0 - # case black_test_patma_142 - match x: -- case bytes(z): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - y = 0 - # case black_test_patma_073 - match x: @@ -16,11 +16,11 @@ y = 1 # case black_test_patma_006 @@ -226,7 +217,7 @@ match x: y = 0 # case black_test_patma_142 match x: - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case bytes(z): y = 0 # case black_test_patma_073 match x: diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap index e048f251a4..8d5c61422e 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap @@ -131,36 +131,6 @@ match bar1: ```diff --- Black +++ Ruff -@@ -5,9 +5,9 @@ - print(b) - case [a as b, c, d, e as f]: - print(f) -- case Point(a as b): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - print(b) -- case Point(int() as x, int() as y): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - print(x, y) - - -@@ -15,7 +15,7 @@ - case: int = re.match(something) - - match re.match(case): -- case type("match", match): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - pass - case match: - pass -@@ -23,7 +23,7 @@ - - def func(match: case, case: match) -> case: - match Something(): -- case func(match, case): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - ... - case another: - ... @@ -32,14 +32,23 @@ match maybe, multiple: case perhaps, 5: @@ -188,21 +158,7 @@ match bar1: pass case _: pass -@@ -59,12 +68,7 @@ - ), - case, - ): -- case case( -- match=case, -- case=re.match( -- loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong -- ), -- ): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - pass - - case [a as match]: -@@ -87,10 +91,10 @@ +@@ -87,7 +96,7 @@ match something: case { "key": key as key_1, @@ -210,12 +166,8 @@ match bar1: + "password": NOT_YET_IMPLEMENTED_PatternMatchOf | (y) as password, }: pass -- case {"maybe": something(complicated as this) as that}: -+ case {"maybe": NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0) as that}: - pass - - -@@ -101,19 +105,17 @@ + case {"maybe": something(complicated as this) as that}: +@@ -101,7 +110,7 @@ case 2 as b, 3 as c: pass @@ -224,20 +176,6 @@ match bar1: pass - match bar1: -- case Foo(aa=Callable() as aa, bb=int()): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - print(bar1.aa, bar1.bb) - case _: - print("no match", "\n") - - - match bar1: -- case Foo( -- normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u -- ): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - pass ``` ## Ruff Output @@ -250,9 +188,9 @@ match something: print(b) case [a as b, c, d, e as f]: print(f) - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case Point(a as b): print(b) - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case Point(int() as x, int() as y): print(x, y) @@ -260,7 +198,7 @@ match = 1 case: int = re.match(something) match re.match(case): - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case type("match", match): pass case match: pass @@ -268,7 +206,7 @@ match re.match(case): def func(match: case, case: match) -> case: match Something(): - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case func(match, case): ... case another: ... @@ -313,7 +251,12 @@ match match( ), case, ): - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case case( + match=case, + case=re.match( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + ), + ): pass case [a as match]: @@ -339,7 +282,7 @@ match something: "password": NOT_YET_IMPLEMENTED_PatternMatchOf | (y) as password, }: pass - case {"maybe": NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0) as that}: + case {"maybe": something(complicated as this) as that}: pass @@ -355,14 +298,16 @@ match something: match bar1: - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case Foo(aa=Callable() as aa, bb=int()): print(bar1.aa, bar1.bb) case _: print("no match", "\n") match bar1: - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case Foo( + normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u + ): pass ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap deleted file mode 100644 index 74ef3ffc47..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap +++ /dev/null @@ -1,357 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py ---- -## Input - -```py -re.match() -match = a -with match() as match: - match = f"{match}" - -re.match() -match = a -with match() as match: - match = f"{match}" - - -def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: - if not target_versions: - # No target_version specified, so try all grammars. - return [ - # Python 3.7+ - pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords, - # Python 3.0-3.6 - pygram.python_grammar_no_print_statement_no_exec_statement, - # Python 2.7 with future print_function import - pygram.python_grammar_no_print_statement, - # Python 2.7 - pygram.python_grammar, - ] - - match match: - case case: - match match: - case case: - pass - - if all(version.is_python2() for version in target_versions): - # Python 2-only code, so try Python 2 grammars. - return [ - # Python 2.7 with future print_function import - pygram.python_grammar_no_print_statement, - # Python 2.7 - pygram.python_grammar, - ] - - re.match() - match = a - with match() as match: - match = f"{match}" - - def test_patma_139(self): - x = False - match x: - case bool(z): - y = 0 - self.assertIs(x, False) - self.assertEqual(y, 0) - self.assertIs(z, x) - - # Python 3-compatible code, so only try Python 3 grammar. - grammars = [] - if supports_feature(target_versions, Feature.PATTERN_MATCHING): - # Python 3.10+ - grammars.append(pygram.python_grammar_soft_keywords) - # If we have to parse both, try to parse async as a keyword first - if not supports_feature( - target_versions, Feature.ASYNC_IDENTIFIERS - ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): - # Python 3.7-3.9 - grammars.append( - pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords - ) - if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS): - # Python 3.0-3.6 - grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement) - - def test_patma_155(self): - x = 0 - y = None - match x: - case 1e1000: - y = 0 - self.assertEqual(x, 0) - self.assertIs(y, None) - - x = range(3) - match x: - case [y, case as x, z]: - w = 0 - - # At least one of the above branches must have been taken, because every Python - # version has exactly one of the two 'ASYNC_*' flags - return grammars - - -def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node: - """Given a string with source, return the lib2to3 Node.""" - if not src_txt.endswith("\n"): - src_txt += "\n" - - grammars = get_grammars(set(target_versions)) - - -re.match() -match = a -with match() as match: - match = f"{match}" - -re.match() -match = a -with match() as match: - match = f"{match}" -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -46,7 +46,7 @@ - def test_patma_139(self): - x = False - match x: -- case bool(z): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - y = 0 - self.assertIs(x, False) - self.assertEqual(y, 0) -``` - -## Ruff Output - -```py -re.match() -match = a -with match() as match: - match = f"{match}" - -re.match() -match = a -with match() as match: - match = f"{match}" - - -def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: - if not target_versions: - # No target_version specified, so try all grammars. - return [ - # Python 3.7+ - pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords, - # Python 3.0-3.6 - pygram.python_grammar_no_print_statement_no_exec_statement, - # Python 2.7 with future print_function import - pygram.python_grammar_no_print_statement, - # Python 2.7 - pygram.python_grammar, - ] - - match match: - case case: - match match: - case case: - pass - - if all(version.is_python2() for version in target_versions): - # Python 2-only code, so try Python 2 grammars. - return [ - # Python 2.7 with future print_function import - pygram.python_grammar_no_print_statement, - # Python 2.7 - pygram.python_grammar, - ] - - re.match() - match = a - with match() as match: - match = f"{match}" - - def test_patma_139(self): - x = False - match x: - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - y = 0 - self.assertIs(x, False) - self.assertEqual(y, 0) - self.assertIs(z, x) - - # Python 3-compatible code, so only try Python 3 grammar. - grammars = [] - if supports_feature(target_versions, Feature.PATTERN_MATCHING): - # Python 3.10+ - grammars.append(pygram.python_grammar_soft_keywords) - # If we have to parse both, try to parse async as a keyword first - if not supports_feature( - target_versions, Feature.ASYNC_IDENTIFIERS - ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): - # Python 3.7-3.9 - grammars.append( - pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords - ) - if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS): - # Python 3.0-3.6 - grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement) - - def test_patma_155(self): - x = 0 - y = None - match x: - case 1e1000: - y = 0 - self.assertEqual(x, 0) - self.assertIs(y, None) - - x = range(3) - match x: - case [y, case as x, z]: - w = 0 - - # At least one of the above branches must have been taken, because every Python - # version has exactly one of the two 'ASYNC_*' flags - return grammars - - -def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node: - """Given a string with source, return the lib2to3 Node.""" - if not src_txt.endswith("\n"): - src_txt += "\n" - - grammars = get_grammars(set(target_versions)) - - -re.match() -match = a -with match() as match: - match = f"{match}" - -re.match() -match = a -with match() as match: - match = f"{match}" -``` - -## Black Output - -```py -re.match() -match = a -with match() as match: - match = f"{match}" - -re.match() -match = a -with match() as match: - match = f"{match}" - - -def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: - if not target_versions: - # No target_version specified, so try all grammars. - return [ - # Python 3.7+ - pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords, - # Python 3.0-3.6 - pygram.python_grammar_no_print_statement_no_exec_statement, - # Python 2.7 with future print_function import - pygram.python_grammar_no_print_statement, - # Python 2.7 - pygram.python_grammar, - ] - - match match: - case case: - match match: - case case: - pass - - if all(version.is_python2() for version in target_versions): - # Python 2-only code, so try Python 2 grammars. - return [ - # Python 2.7 with future print_function import - pygram.python_grammar_no_print_statement, - # Python 2.7 - pygram.python_grammar, - ] - - re.match() - match = a - with match() as match: - match = f"{match}" - - def test_patma_139(self): - x = False - match x: - case bool(z): - y = 0 - self.assertIs(x, False) - self.assertEqual(y, 0) - self.assertIs(z, x) - - # Python 3-compatible code, so only try Python 3 grammar. - grammars = [] - if supports_feature(target_versions, Feature.PATTERN_MATCHING): - # Python 3.10+ - grammars.append(pygram.python_grammar_soft_keywords) - # If we have to parse both, try to parse async as a keyword first - if not supports_feature( - target_versions, Feature.ASYNC_IDENTIFIERS - ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): - # Python 3.7-3.9 - grammars.append( - pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords - ) - if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS): - # Python 3.0-3.6 - grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement) - - def test_patma_155(self): - x = 0 - y = None - match x: - case 1e1000: - y = 0 - self.assertEqual(x, 0) - self.assertIs(y, None) - - x = range(3) - match x: - case [y, case as x, z]: - w = 0 - - # At least one of the above branches must have been taken, because every Python - # version has exactly one of the two 'ASYNC_*' flags - return grammars - - -def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node: - """Given a string with source, return the lib2to3 Node.""" - if not src_txt.endswith("\n"): - src_txt += "\n" - - grammars = get_grammars(set(target_versions)) - - -re.match() -match = a -with match() as match: - match = f"{match}" - -re.match() -match = a -with match() as match: - match = f"{match}" -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_simple.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_simple.py.snap index edbd357b65..a3525af0b8 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_simple.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_simple.py.snap @@ -127,50 +127,15 @@ def where_is(point): current_room = current_room.neighbor(direction) match command.split(): -@@ -60,33 +60,33 @@ - print("Sorry, you can't go that way") - +@@ -62,7 +62,7 @@ match event.get(): -- case Click(position=(x, y)): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case Click(position=(x, y)): handle_click_at(x, y) - case KeyPress(key_name="Q") | Quit(): + case NOT_YET_IMPLEMENTED_PatternMatchOf | (y): game.quit() -- case KeyPress(key_name="up arrow"): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case KeyPress(key_name="up arrow"): game.go_north() -- case KeyPress(): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - pass # Ignore other keystrokes - case other_event: - raise ValueError(f"Unrecognized event: {other_event}") - - match event.get(): -- case Click((x, y), button=Button.LEFT): # This is a left click -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): # This is a left click - handle_click_at(x, y) -- case Click(): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - pass # ignore other clicks - - - def where_is(point): - match point: -- case Point(x=0, y=0): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - print("Origin") -- case Point(x=0, y=y): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - print(f"Y={y}") -- case Point(x=x, y=0): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - print(f"X={x}") -- case Point(): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - print("Somewhere else") - case _: - print("Not a point") ``` ## Ruff Output @@ -238,33 +203,33 @@ match command.split(): print("Sorry, you can't go that way") match event.get(): - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case Click(position=(x, y)): handle_click_at(x, y) case NOT_YET_IMPLEMENTED_PatternMatchOf | (y): game.quit() - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case KeyPress(key_name="up arrow"): game.go_north() - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case KeyPress(): pass # Ignore other keystrokes case other_event: raise ValueError(f"Unrecognized event: {other_event}") match event.get(): - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): # This is a left click + case Click((x, y), button=Button.LEFT): # This is a left click handle_click_at(x, y) - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case Click(): pass # ignore other clicks def where_is(point): match point: - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case Point(x=0, y=0): print("Origin") - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case Point(x=0, y=y): print(f"Y={y}") - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case Point(x=x, y=0): print(f"X={x}") - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case Point(): print("Somewhere else") case _: print("Not a point") diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap index 3f8e3b8feb..d05be40408 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap @@ -65,21 +65,14 @@ match match( ```diff --- Black +++ Ruff -@@ -1,35 +1,34 @@ - match something: -- case b(): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): - print(1 + 1) -- case c( -- very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 -- ): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): +@@ -6,30 +6,35 @@ + ): print(1) -- case c( + case c( - very_complex=True, - perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, -- ): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): ++ very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 + ): print(2) case a: pass @@ -109,10 +102,10 @@ match match( +) re.match() match match(): -- case case( + case case( - arg, # comment -- ): -+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): ++ arg # comment + ): pass ``` @@ -120,11 +113,15 @@ match match( ```py match something: - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case b(): print(1 + 1) - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case c( + very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 + ): print(1) - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case c( + very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 + ): print(2) case a: pass @@ -151,7 +148,9 @@ re.match( ) re.match() match match(): - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case case( + arg # comment + ): pass ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__match.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__match.py.snap index 32daa63f66..9c184bdad0 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__match.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__match.py.snap @@ -116,11 +116,11 @@ def location(point): match point: case Point(x=0, y =0 ) : # fmt: skip print("Origin is the point's location.") - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case Point(x=0, y=y): print(f"Y={y} and the point is on the y-axis.") - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case Point(x=x, y=0): print(f"X={x} and the point is on the x-axis.") - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case Point(): print("The point is located somewhere else on the plane.") case _: print("Not a point") @@ -133,12 +133,9 @@ match points: Point(0, 0) ]: # fmt: skip print("The origin is the only point in the list.") - case [NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0)]: + case [Point(x, y)]: print(f"A single point {x}, {y} is in the list.") - case [ - NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0), - NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0), - ]: + case [Point(0, y1), Point(0, y2)]: print(f"Two points on the Y axis at {y1}, {y2} are in the list.") case _: print("Something else is found in the list.") @@ -158,7 +155,7 @@ match test_variable: match point: case Point(x, y) if x == y: # fmt: skip print(f"The point is located on the diagonal Y=X at {x}.") - case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + case Point(x, y): print(f"Point is not on the diagonal.") ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap index 5f2bb3ae52..2a98a91bd7 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap @@ -270,6 +270,7 @@ match foo: y = 1 + match foo: case [1, 2, *rest]: pass @@ -405,6 +406,64 @@ match foo: b, }: pass + + +match pattern_match_class: + case Point2D( + # own line + ): + ... + + case ( + Point2D + # own line + () + ): + ... + + case Point2D( # end of line line + ): + ... + + case Point2D( # end of line + 0, 0 + ): + ... + + case Point2D(0, 0): + ... + + case Point2D( + ( # end of line + # own line + 0 + ), 0): + ... + + case Point3D(x=0, y=0, z=000000000000000000000000000000000000000000000000000000000000000000000000000000000): + ... + + case Bar(0, a=None, b="hello"): + ... + + case FooBar(# leading +# leading + # leading + # leading + 0 # trailing +# trailing + # trailing + # trailing + ): + ... + + case A( + b # b + = # c + 2 # d + # e + ): + pass ``` ## Output @@ -825,6 +884,73 @@ match foo: **b, }: pass + + +match pattern_match_class: + case Point2D( + # own line + ): + ... + + case ( + Point2D + # own line + () + ): + ... + + case Point2D( # end of line line + ): + ... + + case Point2D( + # end of line + 0, + 0, + ): + ... + + case Point2D(0, 0): + ... + + case Point2D( + ( # end of line + # own line + 0 + ), + 0, + ): + ... + + case Point3D( + x=0, + y=0, + z=000000000000000000000000000000000000000000000000000000000000000000000000000000000, + ): + ... + + case Bar(0, a=None, b="hello"): + ... + + case FooBar( + # leading + # leading + # leading + # leading + 0 # trailing + # trailing + # trailing + # trailing + ): + ... + + case A( + b=# b + # c + 2 # d + # e + ): + pass ```