diff --git a/crates/ruff_dev/src/check_formatter_stability.rs b/crates/ruff_dev/src/check_formatter_stability.rs index 4f07db548f..a499296fe5 100644 --- a/crates/ruff_dev/src/check_formatter_stability.rs +++ b/crates/ruff_dev/src/check_formatter_stability.rs @@ -4,7 +4,7 @@ //! checking entire repositories. #![allow(clippy::print_stdout)] -use anyhow::Context; +use anyhow::{bail, Context}; use clap::Parser; use log::debug; use ruff::resolver::python_files_in_path; @@ -107,7 +107,10 @@ pub(crate) fn check_repo(args: &Args) -> anyhow::Result { ]) .unwrap(); let (paths, _resolver) = python_files_in_path(&cli.files, &pyproject_config, &overrides)?; - assert!(!paths.is_empty(), "no python files in {:?}", cli.files); + + if paths.is_empty() { + bail!("no python files in {:?}", cli.files) + } let mut formatted_counter = 0; let errors = paths diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py b/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py new file mode 100644 index 0000000000..bbe56623c6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py @@ -0,0 +1,67 @@ +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a + if foo + else b, + baz="hello, this is a another value", +) + +imploding_line = ( + 1 + if 1 + 1 == 2 + else 0 +) + +exploding_line = "hello this is a slightly long string" if some_long_value_name_foo_bar_baz else "this one is a little shorter" + +positional_argument_test(some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz) + +def weird_default_argument(x=some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz): + pass + +nested = "hello this is a slightly long string" if (some_long_value_name_foo_bar_baz if + nesting_test_expressions else some_fallback_value_foo_bar_baz) \ + else "this one is a little shorter" + +generator_expression = ( + some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz for some_boolean_variable in some_iterable +) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable + if flat + else ValuesListIterable + ) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py.expect new file mode 100644 index 0000000000..122ea7860d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py.expect @@ -0,0 +1,90 @@ +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a if foo else b, + baz="hello, this is a another value", +) + +imploding_line = 1 if 1 + 1 == 2 else 0 + +exploding_line = ( + "hello this is a slightly long string" + if some_long_value_name_foo_bar_baz + else "this one is a little shorter" +) + +positional_argument_test( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz +) + + +def weird_default_argument( + x=( + some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz + ), +): + pass + + +nested = ( + "hello this is a slightly long string" + if ( + some_long_value_name_foo_bar_baz + if nesting_test_expressions + else some_fallback_value_foo_bar_baz + ) + else "this one is a little shorter" +) + +generator_expression = ( + ( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ) + for some_boolean_variable in some_iterable +) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable if flat else ValuesListIterable + ) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py new file mode 100644 index 0000000000..c5278325db --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py @@ -0,0 +1,6 @@ +def abc (): + return ["hello", "world", + "!"] + +print( "Incorrect formatting" +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py.expect new file mode 100644 index 0000000000..c5278325db --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py.expect @@ -0,0 +1,6 @@ +def abc (): + return ["hello", "world", + "!"] + +print( "Incorrect formatting" +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py new file mode 100644 index 0000000000..d1d1ba1216 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py @@ -0,0 +1,32 @@ +@dataclass +class DebugVisitor(Visitor[T]): + tree_depth: int = 0 + + def visit_default(self, node: LN) -> Iterator[T]: + indent = ' ' * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) + out(f'{indent}{_type}', fg='yellow') + self.tree_depth += 1 + for child in node.children: + yield from self.visit(child) + + self.tree_depth -= 1 + out(f'{indent}/{_type}', fg='yellow', bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) + out(f'{indent}{_type}', fg='blue', nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. + out(f' {node.prefix!r}', fg='green', bold=False, nl=False) + out(f' {node.value!r}', fg='blue', bold=False) + + @classmethod + def show(cls, code: str) -> None: + """Pretty-prints a given string of `code`. + + Convenience method for debugging. + """ + v: DebugVisitor[None] = DebugVisitor() + list(v.visit(lib2to3_parse(code))) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py.expect new file mode 100644 index 0000000000..d1d1ba1216 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py.expect @@ -0,0 +1,32 @@ +@dataclass +class DebugVisitor(Visitor[T]): + tree_depth: int = 0 + + def visit_default(self, node: LN) -> Iterator[T]: + indent = ' ' * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) + out(f'{indent}{_type}', fg='yellow') + self.tree_depth += 1 + for child in node.children: + yield from self.visit(child) + + self.tree_depth -= 1 + out(f'{indent}/{_type}', fg='yellow', bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) + out(f'{indent}{_type}', fg='blue', nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. + out(f' {node.prefix!r}', fg='green', bold=False, nl=False) + out(f' {node.value!r}', fg='blue', bold=False) + + @classmethod + def show(cls, code: str) -> None: + """Pretty-prints a given string of `code`. + + Convenience method for debugging. + """ + v: DebugVisitor[None] = DebugVisitor() + list(v.visit(lib2to3_parse(code))) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py new file mode 100644 index 0000000000..46e37f69ed --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py @@ -0,0 +1,150 @@ +# This file doesn't use the standard decomposition. +# Decorator syntax test cases are separated by double # comments. +# Those before the 'output' comment are valid under the old syntax. +# Those after the 'ouput' comment require PEP614 relaxed syntax. +# Do not remove the double # separator before the first test case, it allows +# the comment before the test case to be ignored. + +## + +@decorator +def f(): + ... + +## + +@decorator() +def f(): + ... + +## + +@decorator(arg) +def f(): + ... + +## + +@decorator(kwarg=0) +def f(): + ... + +## + +@decorator(*args) +def f(): + ... + +## + +@decorator(**kwargs) +def f(): + ... + +## + +@decorator(*args, **kwargs) +def f(): + ... + +## + +@decorator(*args, **kwargs,) +def f(): + ... + +## + +@dotted.decorator +def f(): + ... + +## + +@dotted.decorator(arg) +def f(): + ... + +## + +@dotted.decorator(kwarg=0) +def f(): + ... + +## + +@dotted.decorator(*args) +def f(): + ... + +## + +@dotted.decorator(**kwargs) +def f(): + ... + +## + +@dotted.decorator(*args, **kwargs) +def f(): + ... + +## + +@dotted.decorator(*args, **kwargs,) +def f(): + ... + +## + +@double.dotted.decorator +def f(): + ... + +## + +@double.dotted.decorator(arg) +def f(): + ... + +## + +@double.dotted.decorator(kwarg=0) +def f(): + ... + +## + +@double.dotted.decorator(*args) +def f(): + ... + +## + +@double.dotted.decorator(**kwargs) +def f(): + ... + +## + +@double.dotted.decorator(*args, **kwargs) +def f(): + ... + +## + +@double.dotted.decorator(*args, **kwargs,) +def f(): + ... + +## + +@_(sequence["decorator"]) +def f(): + ... + +## + +@eval("sequence['decorator']") +def f(): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py.expect new file mode 100644 index 0000000000..df17e1e749 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py.expect @@ -0,0 +1,29 @@ +## + +@decorator()() +def f(): + ... + +## + +@(decorator) +def f(): + ... + +## + +@sequence["decorator"] +def f(): + ... + +## + +@decorator[List[str]] +def f(): + ... + +## + +@var := decorator +def f(): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py new file mode 100644 index 0000000000..3116529c65 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py @@ -0,0 +1,123 @@ +class ALonelyClass: + ''' + A multiline class docstring. + ''' + def AnEquallyLonelyMethod(self): + ''' + A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + '''This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not +make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it! + + """ + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): ''' +"hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + ''' + Docstring Docstring Docstring + ''' + pass + + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\ ''' diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py.expect new file mode 100644 index 0000000000..8aefa4b2c2 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py.expect @@ -0,0 +1,123 @@ +class ALonelyClass: + ''' + A multiline class docstring. + ''' + + def AnEquallyLonelyMethod(self): + ''' + A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + '''This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not + make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it!""" + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + ''' + "hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + ''' + Docstring Docstring Docstring + ''' + pass + + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\''' diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py new file mode 100644 index 0000000000..338cc01d33 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py @@ -0,0 +1,10 @@ +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + FR'There was a bug where docstring prefixes would be normalized even with -S.' + + +def do_not_touch_this_prefix3(): + u'''There was a bug where docstring prefixes would be normalized even with -S.''' diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py.expect new file mode 100644 index 0000000000..338cc01d33 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py.expect @@ -0,0 +1,10 @@ +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + FR'There was a bug where docstring prefixes would be normalized even with -S.' + + +def do_not_touch_this_prefix3(): + u'''There was a bug where docstring prefixes would be normalized even with -S.''' diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py new file mode 100644 index 0000000000..106e97214d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py @@ -0,0 +1,3 @@ +# The input source must not contain any Py36-specific syntax (e.g. argument type +# annotations, trailing comma after *rest) or this test becomes invalid. +def long_function_name(argument_one, argument_two, argument_three, argument_four, argument_five, argument_six, *rest): ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py.expect new file mode 100644 index 0000000000..bb26932707 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py.expect @@ -0,0 +1,12 @@ +# The input source must not contain any Py36-specific syntax (e.g. argument type +# annotations, trailing comma after *rest) or this test becomes invalid. +def long_function_name( + argument_one, + argument_two, + argument_three, + argument_four, + argument_five, + argument_six, + *rest, +): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py new file mode 100644 index 0000000000..9c8c40cc96 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py @@ -0,0 +1,30 @@ +from typing import Union + +@bird +def zoo(): ... + +class A: ... +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg : List[str]) -> None: ... + +class C: ... +@hmm +class D: ... +class E: ... + +@baz +def foo() -> None: + ... + +class F (A , C): ... +def spam() -> None: ... + +@overload +def spam(arg: str) -> str: ... + +var : int = 1 + +def eggs() -> Union[str, int]: ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py.expect new file mode 100644 index 0000000000..4349ba0a53 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py.expect @@ -0,0 +1,32 @@ +from typing import Union + +@bird +def zoo(): ... + +class A: ... + +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg: List[str]) -> None: ... + +class C: ... + +@hmm +class D: ... + +class E: ... + +@baz +def foo() -> None: ... + +class F(A, C): ... + +def spam() -> None: ... +@overload +def spam(arg: str) -> str: ... + +var: int = 1 + +def eggs() -> Union[str, int]: ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py new file mode 100644 index 0000000000..4fb342726f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py @@ -0,0 +1,5 @@ +# Regression test for #3427, which reproes only with line length <= 6 +def f(): + """ + x + """ diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py.expect new file mode 100644 index 0000000000..4fb342726f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py.expect @@ -0,0 +1,5 @@ +# Regression test for #3427, which reproes only with line length <= 6 +def f(): + """ + x + """ diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py new file mode 100644 index 0000000000..db3954e3ab --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py @@ -0,0 +1,292 @@ +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." # Third Comment +) + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) +) + +fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." + +fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." + +comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception." + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( + "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." + % "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." + % ("string", "formatting") +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string." +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", # comment after comma +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), # comment after comma +) + +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, ("short string that should have parens stripped"), z +) + +func_with_bad_parens( + ("short string that should have parens stripped"), + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + ("short string that should have parens stripped"), + z, +) + +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + CONCATENATED + + "using the '+' operator." +) +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal[ + "fakse_literal" +] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + + +x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py.expect new file mode 100644 index 0000000000..db3954e3ab --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py.expect @@ -0,0 +1,292 @@ +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." # Third Comment +) + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) +) + +fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." + +fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." + +comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception." + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( + "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." + % "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." + % ("string", "formatting") +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string." +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", # comment after comma +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), # comment after comma +) + +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, ("short string that should have parens stripped"), z +) + +func_with_bad_parens( + ("short string that should have parens stripped"), + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + ("short string that should have parens stripped"), + z, +) + +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + CONCATENATED + + "using the '+' operator." +) +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal[ + "fakse_literal" +] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + + +x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py new file mode 100644 index 0000000000..763909fe59 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py @@ -0,0 +1,3 @@ +# A comment-only file, with no final EOL character +# This triggers https://bugs.python.org/issue2142 +# This is the line without the EOL character diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py.expect new file mode 100644 index 0000000000..763909fe59 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py.expect @@ -0,0 +1,3 @@ +# A comment-only file, with no final EOL character +# This triggers https://bugs.python.org/issue2142 +# This is the line without the EOL character diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py new file mode 100644 index 0000000000..930e29ab56 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py @@ -0,0 +1 @@ +importA;()<<0**0# diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py.expect new file mode 100644 index 0000000000..32e89db2df --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py.expect @@ -0,0 +1,6 @@ +importA +( + () + << 0 + ** 0 +) # diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py new file mode 100644 index 0000000000..86c68e531a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py @@ -0,0 +1,57 @@ +'''''' +'\'' +'"' +"'" +"\"" +"Hello" +"Don't do that" +'Here is a "' +'What\'s the deal here?' +"What's the deal \"here\"?" +"And \"here\"?" +"""Strings with "" in them""" +'''Strings with "" in them''' +'''Here's a "''' +'''Here's a " ''' +'''Just a normal triple +quote''' +f"just a normal {f} string" +f'''This is a triple-quoted {f}-string''' +f'MOAR {" ".join([])}' +f"MOAR {' '.join([])}" +r"raw string ftw" +r'Date d\'expiration:(.*)' +r'Tricky "quote' +r'Not-so-tricky \"quote' +rf'{yay}' +'\n\ +The \"quick\"\n\ +brown fox\n\ +jumps over\n\ +the \'lazy\' dog.\n\ +' +re.compile(r'[\\"]') +"x = ''; y = \"\"" +"x = '''; y = \"\"" +"x = ''''; y = \"\"" +"x = '' ''; y = \"\"" +"x = ''; y = \"\"\"" +"x = '''; y = \"\"\"\"" +"x = ''''; y = \"\"\"\"\"" +"x = '' ''; y = \"\"\"\"\"" +'unnecessary \"\"escaping' +"unnecessary \'\'escaping" +'\\""' +"\\''" +'Lots of \\\\\\\\\'quotes\'' +f'{y * " "} \'{z}\'' +f'{{y * " "}} \'{z}\'' +f'\'{z}\' {y * " "}' +f'{y * x} \'{z}\'' +'\'{z}\' {y * " "}' +'{y * x} \'{z}\'' + +# We must bail out if changing the quotes would introduce backslashes in f-string +# expressions. xref: https://github.com/psf/black/issues/2348 +f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +f"\"{a}\"{'hello' * b}\"{c}\"" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py.expect new file mode 100644 index 0000000000..dce6105acf --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py.expect @@ -0,0 +1,52 @@ +"""""" +"'" +'"' +"'" +'"' +"Hello" +"Don't do that" +'Here is a "' +"What's the deal here?" +'What\'s the deal "here"?' +'And "here"?' +"""Strings with "" in them""" +"""Strings with "" in them""" +'''Here's a "''' +"""Here's a " """ +"""Just a normal triple +quote""" +f"just a normal {f} string" +f"""This is a triple-quoted {f}-string""" +f'MOAR {" ".join([])}' +f"MOAR {' '.join([])}" +r"raw string ftw" +r"Date d\'expiration:(.*)" +r'Tricky "quote' +r"Not-so-tricky \"quote" +rf"{yay}" +"\nThe \"quick\"\nbrown fox\njumps over\nthe 'lazy' dog.\n" +re.compile(r'[\\"]') +"x = ''; y = \"\"" +"x = '''; y = \"\"" +"x = ''''; y = \"\"" +"x = '' ''; y = \"\"" +'x = \'\'; y = """' +'x = \'\'\'; y = """"' +'x = \'\'\'\'; y = """""' +'x = \'\' \'\'; y = """""' +'unnecessary ""escaping' +"unnecessary ''escaping" +'\\""' +"\\''" +"Lots of \\\\\\\\'quotes'" +f'{y * " "} \'{z}\'' +f"{{y * \" \"}} '{z}'" +f'\'{z}\' {y * " "}' +f"{y * x} '{z}'" +"'{z}' {y * \" \"}" +"{y * x} '{z}'" + +# We must bail out if changing the quotes would introduce backslashes in f-string +# expressions. xref: https://github.com/psf/black/issues/2348 +f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +f"\"{a}\"{'hello' * b}\"{c}\"" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py new file mode 100644 index 0000000000..ccf1f94883 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py @@ -0,0 +1,21 @@ +with (CtxManager() as example): + ... + +with (CtxManager1(), CtxManager2()): + ... + +with (CtxManager1() as example, CtxManager2()): + ... + +with (CtxManager1(), CtxManager2() as example): + ... + +with (CtxManager1() as example1, CtxManager2() as example2): + ... + +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager3() as example3, +): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py.expect new file mode 100644 index 0000000000..dfae92c596 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py.expect @@ -0,0 +1,21 @@ +with CtxManager() as example: + ... + +with CtxManager1(), CtxManager2(): + ... + +with CtxManager1() as example, CtxManager2(): + ... + +with CtxManager1(), CtxManager2() as example: + ... + +with CtxManager1() as example1, CtxManager2() as example2: + ... + +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager3() as example3, +): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py new file mode 100644 index 0000000000..97ee194fd3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py @@ -0,0 +1,144 @@ +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +match x: + case -0j: + y = 0 +# case black_test_patma_142 +match x: + case bytes(z): + y = 0 +# case black_test_patma_073 +match x: + case 0 if 0: + y = 0 + case 0 if 1: + y = 1 +# case black_test_patma_006 +match 3: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_049 +match x: + case [0, 1] | [1, 0]: + y = 0 +# case black_check_sequence_then_mapping +match x: + case [*_]: + return "seq" + case {}: + return "map" +# case black_test_patma_035 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +# case black_test_patma_107 +match x: + case 0.25 + 1.75j: + y = 0 +# case black_test_patma_097 +match x: + case -0j: + y = 0 +# case black_test_patma_007 +match 4: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_154 +match x: + case 0 if x: + y = 0 +# case black_test_patma_134 +match x: + case {1: 0}: + y = 0 + case {0: 0}: + y = 1 + case {**z}: + y = 2 +# case black_test_patma_185 +match Seq(): + case [*_]: + y = 0 +# case black_test_patma_063 +match x: + case 1: + y = 0 + case 1: + y = 1 +# case black_test_patma_248 +match x: + case {"foo": bar}: + y = bar +# case black_test_patma_019 +match (0, 1, 2): + case [0, 1, *x, 2]: + y = 0 +# case black_test_patma_052 +match x: + case [0]: + y = 0 + case [1, 0] if (x := x[:0]): + y = 1 + case [1, 0]: + y = 2 +# case black_test_patma_191 +match w: + case [x, y, *_]: + z = 0 +# case black_test_patma_110 +match x: + case -0.25 - 1.75j: + y = 0 +# case black_test_patma_151 +match (x,): + case [y]: + z = 0 +# case black_test_patma_114 +match x: + case A.B.C.D: + y = 0 +# case black_test_patma_232 +match x: + case None: + y = 0 +# case black_test_patma_058 +match x: + case 0: + y = 0 +# case black_test_patma_233 +match x: + case False: + y = 0 +# case black_test_patma_078 +match x: + case []: + y = 0 + case [""]: + y = 1 + case "": + y = 2 +# case black_test_patma_156 +match x: + case z: + y = 0 +# case black_test_patma_189 +match w: + case [x, y, *rest]: + z = 0 +# case black_test_patma_042 +match x: + case (0 as z) | (1 as z) | (2 as z) if z == x % 2: + y = 0 +# case black_test_patma_034 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py.expect new file mode 100644 index 0000000000..97ee194fd3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py.expect @@ -0,0 +1,144 @@ +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +match x: + case -0j: + y = 0 +# case black_test_patma_142 +match x: + case bytes(z): + y = 0 +# case black_test_patma_073 +match x: + case 0 if 0: + y = 0 + case 0 if 1: + y = 1 +# case black_test_patma_006 +match 3: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_049 +match x: + case [0, 1] | [1, 0]: + y = 0 +# case black_check_sequence_then_mapping +match x: + case [*_]: + return "seq" + case {}: + return "map" +# case black_test_patma_035 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +# case black_test_patma_107 +match x: + case 0.25 + 1.75j: + y = 0 +# case black_test_patma_097 +match x: + case -0j: + y = 0 +# case black_test_patma_007 +match 4: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_154 +match x: + case 0 if x: + y = 0 +# case black_test_patma_134 +match x: + case {1: 0}: + y = 0 + case {0: 0}: + y = 1 + case {**z}: + y = 2 +# case black_test_patma_185 +match Seq(): + case [*_]: + y = 0 +# case black_test_patma_063 +match x: + case 1: + y = 0 + case 1: + y = 1 +# case black_test_patma_248 +match x: + case {"foo": bar}: + y = bar +# case black_test_patma_019 +match (0, 1, 2): + case [0, 1, *x, 2]: + y = 0 +# case black_test_patma_052 +match x: + case [0]: + y = 0 + case [1, 0] if (x := x[:0]): + y = 1 + case [1, 0]: + y = 2 +# case black_test_patma_191 +match w: + case [x, y, *_]: + z = 0 +# case black_test_patma_110 +match x: + case -0.25 - 1.75j: + y = 0 +# case black_test_patma_151 +match (x,): + case [y]: + z = 0 +# case black_test_patma_114 +match x: + case A.B.C.D: + y = 0 +# case black_test_patma_232 +match x: + case None: + y = 0 +# case black_test_patma_058 +match x: + case 0: + y = 0 +# case black_test_patma_233 +match x: + case False: + y = 0 +# case black_test_patma_078 +match x: + case []: + y = 0 + case [""]: + y = 1 + case "": + y = 2 +# case black_test_patma_156 +match x: + case z: + y = 0 +# case black_test_patma_189 +match w: + case [x, y, *rest]: + z = 0 +# case black_test_patma_042 +match x: + case (0 as z) | (1 as z) | (2 as z) if z == x % 2: + y = 0 +# case black_test_patma_034 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py new file mode 100644 index 0000000000..0242d264e5 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py @@ -0,0 +1,119 @@ +import match + +match something: + case [a as b]: + print(b) + case [a as b, c, d, e as f]: + print(f) + case Point(a as b): + print(b) + case Point(int() as x, int() as y): + print(x, y) + + +match = 1 +case: int = re.match(something) + +match re.match(case): + case type("match", match): + pass + case match: + pass + + +def func(match: case, case: match) -> case: + match Something(): + case func(match, case): + ... + case another: + ... + + +match maybe, multiple: + case perhaps, 5: + pass + case perhaps, 6,: + pass + + +match more := (than, one), indeed,: + case _, (5, 6): + pass + case [[5], (6)], [7],: + pass + case _: + pass + + +match a, *b, c: + case [*_]: + assert "seq" == _ + case {}: + assert "map" == b + + +match match( + case, + match( + match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match + ), + case, +): + case case( + match=case, + case=re.match( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + ), + ): + pass + + case [a as match]: + pass + + case case: + pass + + +match match: + case case: + pass + + +match a, *b(), c: + case d, *f, g: + pass + + +match something: + case { + "key": key as key_1, + "password": PASS.ONE | PASS.TWO | PASS.THREE as password, + }: + pass + case {"maybe": something(complicated as this) as that}: + pass + + +match something: + case 1 as a: + pass + + case 2 as b, 3 as c: + pass + + case 4 as d, (5 as e), (6 | 7 as g), *h: + pass + + +match bar1: + case Foo(aa=Callable() as aa, bb=int()): + 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 + ): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py.expect new file mode 100644 index 0000000000..0242d264e5 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py.expect @@ -0,0 +1,119 @@ +import match + +match something: + case [a as b]: + print(b) + case [a as b, c, d, e as f]: + print(f) + case Point(a as b): + print(b) + case Point(int() as x, int() as y): + print(x, y) + + +match = 1 +case: int = re.match(something) + +match re.match(case): + case type("match", match): + pass + case match: + pass + + +def func(match: case, case: match) -> case: + match Something(): + case func(match, case): + ... + case another: + ... + + +match maybe, multiple: + case perhaps, 5: + pass + case perhaps, 6,: + pass + + +match more := (than, one), indeed,: + case _, (5, 6): + pass + case [[5], (6)], [7],: + pass + case _: + pass + + +match a, *b, c: + case [*_]: + assert "seq" == _ + case {}: + assert "map" == b + + +match match( + case, + match( + match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match + ), + case, +): + case case( + match=case, + case=re.match( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + ), + ): + pass + + case [a as match]: + pass + + case case: + pass + + +match match: + case case: + pass + + +match a, *b(), c: + case d, *f, g: + pass + + +match something: + case { + "key": key as key_1, + "password": PASS.ONE | PASS.TWO | PASS.THREE as password, + }: + pass + case {"maybe": something(complicated as this) as that}: + pass + + +match something: + case 1 as a: + pass + + case 2 as b, 3 as c: + pass + + case 4 as d, (5 as e), (6 | 7 as g), *h: + pass + + +match bar1: + case Foo(aa=Callable() as aa, bb=int()): + 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 + ): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py new file mode 100644 index 0000000000..00a0e4a677 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py @@ -0,0 +1,107 @@ +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/resources/test/fixtures/black/py_310/pattern_matching_generic.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py.expect new file mode 100644 index 0000000000..00a0e4a677 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py.expect @@ -0,0 +1,107 @@ +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/resources/test/fixtures/black/py_310/pattern_matching_simple.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py new file mode 100644 index 0000000000..5ed62415a4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py @@ -0,0 +1,92 @@ +# Cases sampled from PEP 636 examples + +match command.split(): + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case [action]: + ... # interpret single-verb action + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case ["quit"]: + print("Goodbye!") + quit_game() + case ["look"]: + current_room.describe() + case ["get", obj]: + character.get(obj, current_room) + case ["go", direction]: + current_room = current_room.neighbor(direction) + # The rest of your commands go here + +match command.split(): + case ["drop", *objects]: + for obj in objects: + character.drop(obj, current_room) + # The rest of your commands go here + +match command.split(): + case ["quit"]: + pass + case ["go", direction]: + print("Going:", direction) + case ["drop", *objects]: + print("Dropping: ", *objects) + case _: + print(f"Sorry, I couldn't understand {command!r}") + +match command.split(): + case ["north"] | ["go", "north"]: + current_room = current_room.neighbor("north") + case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: + ... # Code for picking up the given object + +match command.split(): + case ["go", ("north" | "south" | "east" | "west")]: + current_room = current_room.neighbor(...) + # how do I know which direction to go? + +match command.split(): + case ["go", ("north" | "south" | "east" | "west") as direction]: + current_room = current_room.neighbor(direction) + +match command.split(): + case ["go", direction] if direction in current_room.exits: + current_room = current_room.neighbor(direction) + case ["go", _]: + print("Sorry, you can't go that way") + +match event.get(): + case Click(position=(x, y)): + handle_click_at(x, y) + case KeyPress(key_name="Q") | Quit(): + game.quit() + case KeyPress(key_name="up arrow"): + game.go_north() + case KeyPress(): + 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 + handle_click_at(x, y) + case Click(): + pass # ignore other clicks + + +def where_is(point): + match point: + case Point(x=0, y=0): + print("Origin") + case Point(x=0, y=y): + print(f"Y={y}") + case Point(x=x, y=0): + print(f"X={x}") + case Point(): + print("Somewhere else") + case _: + print("Not a point") diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py.expect new file mode 100644 index 0000000000..5ed62415a4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py.expect @@ -0,0 +1,92 @@ +# Cases sampled from PEP 636 examples + +match command.split(): + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case [action]: + ... # interpret single-verb action + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case ["quit"]: + print("Goodbye!") + quit_game() + case ["look"]: + current_room.describe() + case ["get", obj]: + character.get(obj, current_room) + case ["go", direction]: + current_room = current_room.neighbor(direction) + # The rest of your commands go here + +match command.split(): + case ["drop", *objects]: + for obj in objects: + character.drop(obj, current_room) + # The rest of your commands go here + +match command.split(): + case ["quit"]: + pass + case ["go", direction]: + print("Going:", direction) + case ["drop", *objects]: + print("Dropping: ", *objects) + case _: + print(f"Sorry, I couldn't understand {command!r}") + +match command.split(): + case ["north"] | ["go", "north"]: + current_room = current_room.neighbor("north") + case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: + ... # Code for picking up the given object + +match command.split(): + case ["go", ("north" | "south" | "east" | "west")]: + current_room = current_room.neighbor(...) + # how do I know which direction to go? + +match command.split(): + case ["go", ("north" | "south" | "east" | "west") as direction]: + current_room = current_room.neighbor(direction) + +match command.split(): + case ["go", direction] if direction in current_room.exits: + current_room = current_room.neighbor(direction) + case ["go", _]: + print("Sorry, you can't go that way") + +match event.get(): + case Click(position=(x, y)): + handle_click_at(x, y) + case KeyPress(key_name="Q") | Quit(): + game.quit() + case KeyPress(key_name="up arrow"): + game.go_north() + case KeyPress(): + 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 + handle_click_at(x, y) + case Click(): + pass # ignore other clicks + + +def where_is(point): + match point: + case Point(x=0, y=0): + print("Origin") + case Point(x=0, y=y): + print(f"Y={y}") + case Point(x=x, y=0): + print(f"X={x}") + case Point(): + print("Somewhere else") + case _: + print("Not a point") diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py new file mode 100644 index 0000000000..e17f1cdbf6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py @@ -0,0 +1,53 @@ +match something: + case b(): print(1+1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=- 1 + ): print(1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, + ): print(2) + case a: pass + +match( + arg # comment +) + +match( +) + +match( + + +) + +case( + arg # comment +) + +case( +) + +case( + + +) + + +re.match( + something # fast +) +re.match( + + + +) +match match( + + +): + case case( + arg, # comment + ): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py.expect new file mode 100644 index 0000000000..d81fa59e33 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py.expect @@ -0,0 +1,35 @@ +match something: + case b(): + print(1 + 1) + case c( + very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 + ): + print(1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, + ): + print(2) + case a: + pass + +match(arg) # comment + +match() + +match() + +case(arg) # comment + +case() + +case() + + +re.match(something) # fast +re.match() +match match(): + case case( + arg, # comment + ): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py new file mode 100644 index 0000000000..cb82b2d23f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py @@ -0,0 +1,15 @@ +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[a:=0] +x[a:=0, b:=1] +x[5, b:=0] + +# Walruses are allowed inside generator expressions on function calls since 3.10. +if any(match := pattern_error.match(s) for s in buffer): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +f(a := b + c for c in range(10)) +f((a := b + c for c in range(10)), x) +f(y=(a := b + c for c in range(10))) +f(x, (a := b + c for c in range(10)), y=z, **q) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py.expect new file mode 100644 index 0000000000..cb82b2d23f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py.expect @@ -0,0 +1,15 @@ +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[a:=0] +x[a:=0, b:=1] +x[5, b:=0] + +# Walruses are allowed inside generator expressions on function calls since 3.10. +if any(match := pattern_error.match(s) for s in buffer): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +f(a := b + c for c in range(10)) +f((a := b + c for c in range(10)), x) +f(y=(a := b + c for c in range(10))) +f(x, (a := b + c for c in range(10)), y=z, **q) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py new file mode 100644 index 0000000000..629b645fce --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py @@ -0,0 +1,19 @@ +def http_status(status): + + match status: + + case 400: + + return "Bad request" + + case 401: + + return "Unauthorized" + + case 403: + + return "Forbidden" + + case 404: + + return "Not found" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py.expect new file mode 100644 index 0000000000..735169ef52 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py.expect @@ -0,0 +1,13 @@ +def http_status(status): + match status: + case 400: + return "Bad request" + + case 401: + return "Unauthorized" + + case 403: + return "Forbidden" + + case 404: + return "Not found" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py new file mode 100644 index 0000000000..8fc8e059ed --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py @@ -0,0 +1,27 @@ +for x in *a, *b: + print(x) + +for x in a, b, *c: + print(x) + +for x in *a, b, c: + print(x) + +for x in *a, b, *c: + print(x) + +async for x in *a, *b: + print(x) + +async for x in *a, b, *c: + print(x) + +async for x in a, b, *c: + print(x) + +async for x in ( + *loooooooooooooooooooooong, + very, + *loooooooooooooooooooooooooooooooooooooooooooooooong, +): + print(x) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py.expect new file mode 100644 index 0000000000..8fc8e059ed --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py.expect @@ -0,0 +1,27 @@ +for x in *a, *b: + print(x) + +for x in a, b, *c: + print(x) + +for x in *a, b, c: + print(x) + +for x in *a, b, *c: + print(x) + +async for x in *a, *b: + print(x) + +async for x in *a, b, *c: + print(x) + +async for x in a, b, *c: + print(x) + +async for x in ( + *loooooooooooooooooooooong, + very, + *loooooooooooooooooooooooooooooooooooooooooooooooong, +): + print(x) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py new file mode 100644 index 0000000000..387c0816f4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py @@ -0,0 +1,53 @@ +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py.expect new file mode 100644 index 0000000000..387c0816f4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py.expect @@ -0,0 +1,53 @@ +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py new file mode 100644 index 0000000000..1c5918d17d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py @@ -0,0 +1,55 @@ +try: + raise OSError("blah") +except * ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except *ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except *(Exception): + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except \ + *TypeError as e: + tes = e + raise + except * ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except *(TypeError, ValueError, *OTHER_EXCEPTIONS) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except\ + * OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py.expect new file mode 100644 index 0000000000..c0d06dbfe1 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py.expect @@ -0,0 +1,53 @@ +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError, *OTHER_EXCEPTIONS) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py new file mode 100644 index 0000000000..6da4ba68d6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3.6 + +x = 123456789 +x = 123456 +x = .1 +x = 1. +x = 1E+1 +x = 1E-1 +x = 1.000_000_01 +x = 123456789.123456789 +x = 123456789.123456789E123456789 +x = 123456789E123456789 +x = 123456789J +x = 123456789.123456789J +x = 0XB1ACC +x = 0B1011 +x = 0O777 +x = 0.000000006 +x = 10000 +x = 133333 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py.expect new file mode 100644 index 0000000000..e263924b4e --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py.expect @@ -0,0 +1,20 @@ +#!/usr/bin/env python3.6 + +x = 123456789 +x = 123456 +x = 0.1 +x = 1.0 +x = 1e1 +x = 1e-1 +x = 1.000_000_01 +x = 123456789.123456789 +x = 123456789.123456789e123456789 +x = 123456789e123456789 +x = 123456789j +x = 123456789.123456789j +x = 0xB1ACC +x = 0b1011 +x = 0o777 +x = 0.000000006 +x = 10000 +x = 133333 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py new file mode 100644 index 0000000000..d77116a832 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3.6 + +x = 123456789 +x = 1_2_3_4_5_6_7 +x = 1E+1 +x = 0xb1acc +x = 0.00_00_006 +x = 12_34_567J +x = .1_2 +x = 1_2. diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py.expect new file mode 100644 index 0000000000..a81ada11e5 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py.expect @@ -0,0 +1,10 @@ +#!/usr/bin/env python3.6 + +x = 123456789 +x = 1_2_3_4_5_6_7 +x = 1e1 +x = 0xB1ACC +x = 0.00_00_006 +x = 12_34_567j +x = 0.1_2 +x = 1_2.0 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py new file mode 100644 index 0000000000..01fd7eede3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3.7 + + +def f(): + return (i * 2 async for i in arange(42)) + + +def g(): + return ( + something_long * something_long + async for something_long in async_generator(with_an_argument) + ) + + +async def func(): + if test: + out_batched = [ + i + async for i in aitertools._async_map( + self.async_inc, arange(8), batch_size=3 + ) + ] + + +def awaited_generator_value(n): + return (await awaitable for awaitable in awaitable_list) + + +def make_arange(n): + return (i * 2 for i in range(n) if await wrap(i)) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py.expect new file mode 100644 index 0000000000..01fd7eede3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py.expect @@ -0,0 +1,30 @@ +#!/usr/bin/env python3.7 + + +def f(): + return (i * 2 async for i in arange(42)) + + +def g(): + return ( + something_long * something_long + async for something_long in async_generator(with_an_argument) + ) + + +async def func(): + if test: + out_batched = [ + i + async for i in aitertools._async_map( + self.async_inc, arange(8), batch_size=3 + ) + ] + + +def awaited_generator_value(n): + return (await awaitable for awaitable in awaitable_list) + + +def make_arange(n): + return (i * 2 for i in range(n) if await wrap(i)) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py new file mode 100644 index 0000000000..ca8f7ab1d9 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py @@ -0,0 +1,44 @@ +def positional_only_arg(a, /): + pass + + +def all_markers(a, b, /, c, d, *, e, f): + pass + + +def all_markers_with_args_and_kwargs( + a_long_one, + b_long_one, + /, + c_long_one, + d_long_one, + *args, + e_long_one, + f_long_one, + **kwargs, +): + pass + + +def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5): + pass + + +def long_one_with_long_parameter_names( + but_all_of_them, + are_positional_only, + arguments_mmmmkay, + so_this_is_only_valid_after, + three_point_eight, + /, +): + pass + + +lambda a, /: a + +lambda a, b, /, c, d, *, e, f: a + +lambda a, b, /, c, d, *args, e, f, **kwargs: args + +lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py.expect new file mode 100644 index 0000000000..ca8f7ab1d9 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py.expect @@ -0,0 +1,44 @@ +def positional_only_arg(a, /): + pass + + +def all_markers(a, b, /, c, d, *, e, f): + pass + + +def all_markers_with_args_and_kwargs( + a_long_one, + b_long_one, + /, + c_long_one, + d_long_one, + *args, + e_long_one, + f_long_one, + **kwargs, +): + pass + + +def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5): + pass + + +def long_one_with_long_parameter_names( + but_all_of_them, + are_positional_only, + arguments_mmmmkay, + so_this_is_only_valid_after, + three_point_eight, + /, +): + pass + + +lambda a, /: a + +lambda a, b, /, c, d, *, e, f: a + +lambda a, b, /, c, d, *args, e, f, **kwargs: args + +lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py new file mode 100644 index 0000000000..d41805f1cb --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py @@ -0,0 +1,47 @@ +(a := 1) +(a := a) +if (match := pattern.search(data)) is None: + pass +if match := pattern.search(data): + pass +[y := f(x), y**2, y**3] +filtered_data = [y for x in data if (y := f(x)) is None] +(y := f(x)) +y0 = (y1 := f(x)) +foo(x=(y := f(x))) + + +def foo(answer=(p := 42)): + pass + + +def foo(answer: (p := 42) = 5): + pass + + +lambda: (x := 1) +(x := lambda: 1) +(x := lambda: (y := 1)) +lambda line: (m := re.match(pattern, line)) and m.group(1) +x = (y := 0) +(z := (y := (x := 0))) +(info := (name, phone, *rest)) +(x := 1, 2) +(total := total + tax) +len(lines := f.readlines()) +foo(x := 3, cat="vector") +foo(cat=(category := "vector")) +if any(len(longline := l) >= 100 for l in lines): + print(longline) +if env_base := os.environ.get("PYTHONUSERBASE", None): + return env_base +if self._is_special and (ans := self._check_nans(context=context)): + return ans +foo(b := 2, a=1) +foo((b := 2), a=1) +foo(c=(b := 2), a=1) + +while x := f(x): + pass +while x := f(x): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py.expect new file mode 100644 index 0000000000..d41805f1cb --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py.expect @@ -0,0 +1,47 @@ +(a := 1) +(a := a) +if (match := pattern.search(data)) is None: + pass +if match := pattern.search(data): + pass +[y := f(x), y**2, y**3] +filtered_data = [y for x in data if (y := f(x)) is None] +(y := f(x)) +y0 = (y1 := f(x)) +foo(x=(y := f(x))) + + +def foo(answer=(p := 42)): + pass + + +def foo(answer: (p := 42) = 5): + pass + + +lambda: (x := 1) +(x := lambda: 1) +(x := lambda: (y := 1)) +lambda line: (m := re.match(pattern, line)) and m.group(1) +x = (y := 0) +(z := (y := (x := 0))) +(info := (name, phone, *rest)) +(x := 1, 2) +(total := total + tax) +len(lines := f.readlines()) +foo(x := 3, cat="vector") +foo(cat=(category := "vector")) +if any(len(longline := l) >= 100 for l in lines): + print(longline) +if env_base := os.environ.get("PYTHONUSERBASE", None): + return env_base +if self._is_special and (ans := self._check_nans(context=context)): + return ans +foo(b := 2, a=1) +foo((b := 2), a=1) +foo(c=(b := 2), a=1) + +while x := f(x): + pass +while x := f(x): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py new file mode 100644 index 0000000000..391b52f8ce --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3.8 + + +def starred_return(): + my_list = ["value2", "value3"] + return "value1", *my_list + + +def starred_yield(): + my_list = ["value2", "value3"] + yield "value1", *my_list + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +a : Tuple[ str, int] = "1", 2 +a: Tuple[int , ... ] = b, *c, d +def t(): + a : str = yield "a" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py.expect new file mode 100644 index 0000000000..5df012410a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py.expect @@ -0,0 +1,21 @@ +#!/usr/bin/env python3.8 + + +def starred_return(): + my_list = ["value2", "value3"] + return "value1", *my_list + + +def starred_yield(): + my_list = ["value2", "value3"] + yield "value1", *my_list + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +a: Tuple[str, int] = "1", 2 +a: Tuple[int, ...] = b, *c, d + + +def t(): + a: str = yield "a" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py new file mode 100644 index 0000000000..b8b081b8c4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py @@ -0,0 +1,7 @@ +# Unparenthesized walruses are now allowed in set literals & set comprehensions +# since Python 3.9 +{x := 1, 2, 3} +{x4 := x**5 for x in range(7)} +# We better not remove the parentheses here (since it's a 3.10 feature) +x[(a := 1)] +x[(a := 1), (b := 3)] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py.expect new file mode 100644 index 0000000000..b8b081b8c4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py.expect @@ -0,0 +1,7 @@ +# Unparenthesized walruses are now allowed in set literals & set comprehensions +# since Python 3.9 +{x := 1, 2, 3} +{x4 := x**5 for x in range(7)} +# We better not remove the parentheses here (since it's a 3.10 feature) +x[(a := 1)] +x[(a := 1), (b := 3)] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py new file mode 100644 index 0000000000..227faca09a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3.9 + +@relaxed_decorator[0] +def f(): + ... + +@relaxed_decorator[extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length] +def f(): + ... + +@extremely_long_variable_name_that_doesnt_fit := complex.expression(with_long="arguments_value_that_wont_fit_at_the_end_of_the_line") +def f(): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py.expect new file mode 100644 index 0000000000..4af4beebb2 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py.expect @@ -0,0 +1,20 @@ +#!/usr/bin/env python3.9 + + +@relaxed_decorator[0] +def f(): + ... + + +@relaxed_decorator[ + extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length +] +def f(): + ... + + +@extremely_long_variable_name_that_doesnt_fit := complex.expression( + with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" +) +def f(): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py new file mode 100644 index 0000000000..9634bab444 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py @@ -0,0 +1,54 @@ +with (open("bla.txt")): + pass + +with (open("bla.txt")), (open("bla.txt")): + pass + +with (open("bla.txt") as f): + pass + +# Remove brackets within alias expression +with (open("bla.txt")) as f: + pass + +# Remove brackets around one-line context managers +with (open("bla.txt") as f, (open("x"))): + pass + +with ((open("bla.txt")) as f, open("x")): + pass + +with (CtxManager1() as example1, CtxManager2() as example2): + ... + +# Brackets remain when using magic comma +with (CtxManager1() as example1, CtxManager2() as example2,): + ... + +# Brackets remain for multi-line context managers +with (CtxManager1() as example1, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2): + ... + +# Don't touch assignment expressions +with (y := open("./test.py")) as f: + pass + +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with (((open("bla.txt")))): + pass + +with (((open("bla.txt")))), (((open("bla.txt")))): + pass + +with (((open("bla.txt")))) as f: + pass + +with ((((open("bla.txt")))) as f): + pass + +with ((((CtxManager1()))) as example1, (((CtxManager2()))) as example2): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py.expect new file mode 100644 index 0000000000..e70d01b18d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py.expect @@ -0,0 +1,63 @@ +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +# Remove brackets within alias expression +with open("bla.txt") as f: + pass + +# Remove brackets around one-line context managers +with open("bla.txt") as f, open("x"): + pass + +with open("bla.txt") as f, open("x"): + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... + +# Brackets remain when using magic comma +with ( + CtxManager1() as example1, + CtxManager2() as example2, +): + ... + +# Brackets remain for multi-line context managers +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, +): + ... + +# Don't touch assignment expressions +with (y := open("./test.py")) as f: + pass + +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +with open("bla.txt") as f: + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py.expect index 4a94e7ad93..c34daaf6f0 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py.expect @@ -93,4 +93,4 @@ async def wat(): # Some closing comments. # Maybe Vim or Emacs directives for formatting. -# Who knows. \ No newline at end of file +# Who knows. diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py.expect index 08be5ea501..16160f98f3 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py.expect @@ -1,3 +1,4 @@ +... "some_string" b"\\xa3" Name @@ -114,7 +115,7 @@ call( arg, another, kwarg="hey", - **kwargs, + **kwargs ) # note: no trailing comma pre-3.6 call(*gidgets[:2]) call(a, *gidgets[:2]) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py.expect index 8631f8eaaa..2534e5cc1d 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py.expect @@ -5,7 +5,6 @@ import sys from third_party import X, Y, Z from library import some_connection, some_decorator - # fmt: off from third_party import (X, Y, Z) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py new file mode 100644 index 0000000000..8b3c0bc662 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py @@ -0,0 +1,19 @@ +# Regression test for https://github.com/psf/black/issues/3438 + +import ast +import collections # fmt: skip +import dataclasses +# fmt: off +import os +# fmt: on +import pathlib + +import re # fmt: skip +import secrets + +# fmt: off +import sys +# fmt: on + +import tempfile +import zoneinfo diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py.expect new file mode 100644 index 0000000000..8b3c0bc662 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py.expect @@ -0,0 +1,19 @@ +# Regression test for https://github.com/psf/black/issues/3438 + +import ast +import collections # fmt: skip +import dataclasses +# fmt: off +import os +# fmt: on +import pathlib + +import re # fmt: skip +import secrets + +# fmt: off +import sys +# fmt: on + +import tempfile +import zoneinfo diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py index b778ec2879..190cd6294a 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py @@ -7,3 +7,5 @@ f"{f'''{'nested'} inner'''} outer" f"\"{f'{nested} inner'}\" outer" f"space between opening braces: { {a for a in (1, 2, 3)}}" f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py.expect index 0c6538c5bf..488d786e39 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py.expect @@ -7,3 +7,5 @@ f"{f'''{'nested'} inner'''} outer" f"\"{f'{nested} inner'}\" outer" f"space between opening braces: { {a for a in (1, 2, 3)}}" f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py index bc41e08a16..e94c5c5ace 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py @@ -34,7 +34,7 @@ def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r'' def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... def spaces2(result= _core.Value(None)): assert fut is self._read_fut, (fut, self._read_fut) - + # EMPTY LINE WITH WHITESPACE (this comment will be removed) def example(session): result = session.query(models.Customer.id).filter( models.Customer.account_id == account_id, diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py new file mode 100644 index 0000000000..2ff5ca4829 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py @@ -0,0 +1,6 @@ +def some_very_long_name_function() -> my_module.Asdf | my_module.AnotherType | my_module.YetAnotherType | None: + pass + + +def some_very_long_name_function() -> my_module.Asdf | my_module.AnotherType | my_module.YetAnotherType | my_module.EvenMoreType | None: + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py.expect new file mode 100644 index 0000000000..1629cc693b --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py.expect @@ -0,0 +1,14 @@ +def some_very_long_name_function() -> ( + my_module.Asdf | my_module.AnotherType | my_module.YetAnotherType | None +): + pass + + +def some_very_long_name_function() -> ( + my_module.Asdf + | my_module.AnotherType + | my_module.YetAnotherType + | my_module.EvenMoreType + | None +): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py index 63885bb872..165117cdcb 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py @@ -29,35 +29,3 @@ ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] ham[lower + offset : upper + offset] - -slice[::, ::] -slice[ - # A - : - # B - : - # C -] -slice[ - # A - 1: - # B - 2: - # C - 3 -] - -slice[ - # A - 1 - + 2 : - # B - 3 : - # C - 4 -] -x[ - 1: # A - 2: # B - 3 # C -] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py.expect index 61218829e4..165117cdcb 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py.expect @@ -29,31 +29,3 @@ ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] ham[lower + offset : upper + offset] - -slice[::, ::] -slice[ - # A - : - # B - : - # C -] -slice[ - # A - 1: - # B - 2: - # C - 3 -] - -slice[ - # A - 1 - + 2 : - # B - 3 : - # C - 4 -] -x[1:2:3] # A # B # C diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/whitespace.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/whitespace.py.expect index e69de29bb2..8b13789179 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/whitespace.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/whitespace.py.expect @@ -0,0 +1 @@ + diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py b/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py new file mode 100644 index 0000000000..70e2fbe328 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py @@ -0,0 +1,3 @@ +def foo( + # type: Foo + x): pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py.expect new file mode 100644 index 0000000000..764f7cedd8 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py.expect @@ -0,0 +1,5 @@ +def foo( + # type: Foo + x, +): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/import_black_tests.py b/crates/ruff_python_formatter/resources/test/fixtures/import_black_tests.py new file mode 100755 index 0000000000..6cad807da4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/import_black_tests.py @@ -0,0 +1,113 @@ +#!/usr/bin/python3 + +from __future__ import annotations + +import argparse +from pathlib import Path + + +def import_fixture(fixture: Path, fixture_set: str): + """ + Imports a single fixture by writing the input and expected output to the black directory. + """ + + output_directory = Path(__file__).parent.joinpath("black").joinpath(fixture_set) + output_directory.mkdir(parents=True, exist_ok=True) + + fixture_path = output_directory.joinpath(fixture.name) + expect_path = fixture_path.with_suffix(".py.expect") + + with ( + fixture.open("r") as black_file, + fixture_path.open("w") as fixture_file, + expect_path.open("w") as expect_file + ): + lines = iter(black_file) + expected = [] + input = [] + + for line in lines: + if line.rstrip() == "# output": + expected = list(lines) + break + else: + input.append(line) + + if not expected: + # If there's no output marker, tread the whole file as already pre-formatted + expected = input + + fixture_file.write("".join(input).strip() + "\n") + expect_file.write("".join(expected).strip() + "\n") + + +# The name of the folders in the `data` for which the tests should be imported +FIXTURE_SETS = [ + "py_36", + "py_37", + "py_38", + "py_39", + "py_310", + "py_311", + "simple_cases", + "miscellaneous", + ".", + "type_comments" +] + +# Tests that ruff doesn't fully support yet and, therefore, should not be imported +IGNORE_LIST = [ + "pep_572_remove_parens.py", # Reformatting bugs + "pep_646.py", # Rust Python parser bug + + # Contain syntax errors + "async_as_identifier.py", + "invalid_header.py", + "pattern_matching_invalid.py", + + # Python 2 + "python2_detection.py" +] + + +def import_fixtures(black_dir: str): + """Imports all the black fixtures""" + + test_directory = Path(black_dir, "tests/data") + + if not test_directory.exists(): + print( + "Black directory does not contain a 'tests/data' directory. Does the directory point to a full black " + "checkout (git clone https://github.com/psf/black.git)?") + return + + for fixture_set in FIXTURE_SETS: + fixture_directory = test_directory.joinpath(fixture_set) + fixtures = fixture_directory.glob("*.py") + + if not fixtures: + print(f"Fixture set '{fixture_set}' contains no python files") + return + + for fixture in fixtures: + if fixture.name in IGNORE_LIST: + print(f"Ignoring fixture '{fixture}") + continue + + print(f"Importing fixture '{fixture}") + import_fixture(fixture, fixture_set) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description="Imports the test suite from black.", + epilog="import_black_tests.py " + ) + + parser.add_argument("black_dir", type=Path) + + args = parser.parse_args() + + black_dir = args.black_dir + + import_fixtures(black_dir) diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs index 43a94051e7..d312ff7de6 100644 --- a/crates/ruff_python_formatter/tests/fixtures.rs +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -12,7 +12,12 @@ fn black_compatibility() { let content = fs::read_to_string(input_path).unwrap(); let options = PyFormatOptions::default(); - let printed = format_module(&content, options.clone()).expect("Formatting to succeed"); + let printed = format_module(&content, options.clone()).unwrap_or_else(|err| { + panic!( + "Formatting of {} to succeed but encountered error {err}", + input_path.display() + ) + }); let expected_path = input_path.with_extension("py.expect"); let expected_output = fs::read_to_string(&expected_path) @@ -20,7 +25,7 @@ fn black_compatibility() { let formatted_code = printed.as_code(); - ensure_stability_when_formatting_twice(formatted_code, options); + ensure_stability_when_formatting_twice(formatted_code, options, input_path); if formatted_code == expected_output { // Black and Ruff formatting matches. Delete any existing snapshot files because the Black output @@ -95,7 +100,7 @@ fn format() { let printed = format_module(&content, options.clone()).expect("Formatting to succeed"); let formatted_code = printed.as_code(); - ensure_stability_when_formatting_twice(formatted_code, options); + ensure_stability_when_formatting_twice(formatted_code, options, input_path); let mut snapshot = format!("## Input\n{}", CodeFrame::new("py", &content)); @@ -112,7 +117,7 @@ fn format() { format_module(&content, options.clone()).expect("Formatting to succeed"); let formatted_code = printed.as_code(); - ensure_stability_when_formatting_twice(formatted_code, options.clone()); + ensure_stability_when_formatting_twice(formatted_code, options.clone(), input_path); writeln!( snapshot, @@ -128,7 +133,7 @@ fn format() { let printed = format_module(&content, options.clone()).expect("Formatting to succeed"); let formatted_code = printed.as_code(); - ensure_stability_when_formatting_twice(formatted_code, options); + ensure_stability_when_formatting_twice(formatted_code, options, input_path); writeln!( snapshot, @@ -151,13 +156,18 @@ fn format() { } /// Format another time and make sure that there are no changes anymore -fn ensure_stability_when_formatting_twice(formatted_code: &str, options: PyFormatOptions) { +fn ensure_stability_when_formatting_twice( + formatted_code: &str, + options: PyFormatOptions, + input_path: &Path, +) { let reformatted = match format_module(formatted_code, options) { Ok(reformatted) => reformatted, Err(err) => { panic!( - "Expected formatted code to be valid syntax: {err}:\ + "Expected formatted code of {} to be valid syntax: {err}:\ \n---\n{formatted_code}---\n", + input_path.display() ); } }; @@ -168,7 +178,7 @@ fn ensure_stability_when_formatting_twice(formatted_code: &str, options: PyForma .header("Formatted once", "Formatted twice") .to_string(); panic!( - r#"Reformatting the formatted code a second time resulted in formatting changes. + r#"Reformatting the formatted code of {} a second time resulted in formatting changes. --- {diff}--- @@ -179,7 +189,8 @@ Formatted once: Formatted twice: --- {}---"#, - reformatted.as_code() + input_path.display(), + reformatted.as_code(), ); } } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap new file mode 100644 index 0000000000..5ab4cf643f --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap @@ -0,0 +1,334 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py +--- +## Input + +```py +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a + if foo + else b, + baz="hello, this is a another value", +) + +imploding_line = ( + 1 + if 1 + 1 == 2 + else 0 +) + +exploding_line = "hello this is a slightly long string" if some_long_value_name_foo_bar_baz else "this one is a little shorter" + +positional_argument_test(some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz) + +def weird_default_argument(x=some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz): + pass + +nested = "hello this is a slightly long string" if (some_long_value_name_foo_bar_baz if + nesting_test_expressions else some_fallback_value_foo_bar_baz) \ + else "this one is a little shorter" + +generator_expression = ( + some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz for some_boolean_variable in some_iterable +) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable + if flat + else ValuesListIterable + ) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,90 +1,48 @@ + long_kwargs_single_line = my_function( + foo="test, this is a sample value", +- bar=( +- some_long_value_name_foo_bar_baz +- if some_boolean_variable +- else some_fallback_value_foo_bar_baz +- ), ++ bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + baz="hello, this is a another value", + ) + + multiline_kwargs_indented = my_function( + foo="test, this is a sample value", +- bar=( +- some_long_value_name_foo_bar_baz +- if some_boolean_variable +- else some_fallback_value_foo_bar_baz +- ), ++ bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + baz="hello, this is a another value", + ) + + imploding_kwargs = my_function( + foo="test, this is a sample value", +- bar=a if foo else b, ++ bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + baz="hello, this is a another value", + ) + +-imploding_line = 1 if 1 + 1 == 2 else 0 ++imploding_line = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + +-exploding_line = ( +- "hello this is a slightly long string" +- if some_long_value_name_foo_bar_baz +- else "this one is a little shorter" +-) ++exploding_line = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + + positional_argument_test( +- some_long_value_name_foo_bar_baz +- if some_boolean_variable +- else some_fallback_value_foo_bar_baz ++ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + ) + + + def weird_default_argument( +- x=( +- some_long_value_name_foo_bar_baz +- if SOME_CONSTANT +- else some_fallback_value_foo_bar_baz +- ), ++ x=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + ): + pass + + +-nested = ( +- "hello this is a slightly long string" +- if ( +- some_long_value_name_foo_bar_baz +- if nesting_test_expressions +- else some_fallback_value_foo_bar_baz +- ) +- else "this one is a little shorter" +-) ++nested = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + +-generator_expression = ( +- ( +- some_long_value_name_foo_bar_baz +- if some_boolean_variable +- else some_fallback_value_foo_bar_baz +- ) +- for some_boolean_variable in some_iterable +-) ++generator_expression = (i for i in []) + + + def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) +- return " ".join( +- sql +- for sql in ( +- "LIMIT %d" % limit if limit else None, +- ("OFFSET %d" % offset) if offset else None, +- ) +- if sql +- ) ++ return " ".join((i for i in [])) + + + def something(): + clone._iterable_class = ( +- NamedValuesListIterable +- if named +- else FlatValuesListIterable if flat else ValuesListIterable ++ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + ) +``` + +## Ruff Output + +```py +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, + baz="hello, this is a another value", +) + +imploding_line = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + +exploding_line = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + +positional_argument_test( + NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false +) + + +def weird_default_argument( + x=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false +): + pass + + +nested = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + +generator_expression = (i for i in []) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join((i for i in [])) + + +def something(): + clone._iterable_class = ( + NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false + ) +``` + +## Black Output + +```py +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a if foo else b, + baz="hello, this is a another value", +) + +imploding_line = 1 if 1 + 1 == 2 else 0 + +exploding_line = ( + "hello this is a slightly long string" + if some_long_value_name_foo_bar_baz + else "this one is a little shorter" +) + +positional_argument_test( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz +) + + +def weird_default_argument( + x=( + some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz + ), +): + pass + + +nested = ( + "hello this is a slightly long string" + if ( + some_long_value_name_foo_bar_baz + if nesting_test_expressions + else some_fallback_value_foo_bar_baz + ) + else "this one is a little shorter" +) + +generator_expression = ( + ( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ) + for some_boolean_variable in some_iterable +) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable if flat else ValuesListIterable + ) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__blackd_diff.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__blackd_diff.py.snap new file mode 100644 index 0000000000..835d6e4d60 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__blackd_diff.py.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py +--- +## Input + +```py +def abc (): + return ["hello", "world", + "!"] + +print( "Incorrect formatting" +) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,6 +1,5 @@ +-def abc (): +- return ["hello", "world", +- "!"] ++def abc(): ++ return ["hello", "world", "!"] + +-print( "Incorrect formatting" +-) ++ ++print("Incorrect formatting") +``` + +## Ruff Output + +```py +def abc(): + return ["hello", "world", "!"] + + +print("Incorrect formatting") +``` + +## Black Output + +```py +def abc (): + return ["hello", "world", + "!"] + +print( "Incorrect formatting" +) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap new file mode 100644 index 0000000000..0e00e30129 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap @@ -0,0 +1,167 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py +--- +## Input + +```py +@dataclass +class DebugVisitor(Visitor[T]): + tree_depth: int = 0 + + def visit_default(self, node: LN) -> Iterator[T]: + indent = ' ' * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) + out(f'{indent}{_type}', fg='yellow') + self.tree_depth += 1 + for child in node.children: + yield from self.visit(child) + + self.tree_depth -= 1 + out(f'{indent}/{_type}', fg='yellow', bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) + out(f'{indent}{_type}', fg='blue', nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. + out(f' {node.prefix!r}', fg='green', bold=False, nl=False) + out(f' {node.value!r}', fg='blue', bold=False) + + @classmethod + def show(cls, code: str) -> None: + """Pretty-prints a given string of `code`. + + Convenience method for debugging. + """ + v: DebugVisitor[None] = DebugVisitor() + list(v.visit(lib2to3_parse(code))) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,26 +1,26 @@ + @dataclass + class DebugVisitor(Visitor[T]): +- tree_depth: int = 0 ++ NOT_YET_IMPLEMENTED_StmtAnnAssign + + def visit_default(self, node: LN) -> Iterator[T]: +- indent = ' ' * (2 * self.tree_depth) ++ indent = " " * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) +- out(f'{indent}{_type}', fg='yellow') +- self.tree_depth += 1 ++ out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow") ++ NOT_YET_IMPLEMENTED_StmtAugAssign + for child in node.children: +- yield from self.visit(child) ++ NOT_YET_IMPLEMENTED_ExprYieldFrom + +- self.tree_depth -= 1 +- out(f'{indent}/{_type}', fg='yellow', bold=False) ++ NOT_YET_IMPLEMENTED_StmtAugAssign ++ out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow", bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) +- out(f'{indent}{_type}', fg='blue', nl=False) ++ out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="blue", nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. +- out(f' {node.prefix!r}', fg='green', bold=False, nl=False) +- out(f' {node.value!r}', fg='blue', bold=False) ++ out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="green", bold=False, nl=False) ++ out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="blue", bold=False) + + @classmethod + def show(cls, code: str) -> None: +@@ -28,5 +28,5 @@ + + Convenience method for debugging. + """ +- v: DebugVisitor[None] = DebugVisitor() ++ NOT_YET_IMPLEMENTED_StmtAnnAssign + list(v.visit(lib2to3_parse(code))) +``` + +## Ruff Output + +```py +@dataclass +class DebugVisitor(Visitor[T]): + NOT_YET_IMPLEMENTED_StmtAnnAssign + + def visit_default(self, node: LN) -> Iterator[T]: + indent = " " * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) + out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow") + NOT_YET_IMPLEMENTED_StmtAugAssign + for child in node.children: + NOT_YET_IMPLEMENTED_ExprYieldFrom + + NOT_YET_IMPLEMENTED_StmtAugAssign + out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="yellow", bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) + out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="blue", nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. + out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="green", bold=False, nl=False) + out(NOT_YET_IMPLEMENTED_ExprJoinedStr, fg="blue", bold=False) + + @classmethod + def show(cls, code: str) -> None: + """Pretty-prints a given string of `code`. + + Convenience method for debugging. + """ + NOT_YET_IMPLEMENTED_StmtAnnAssign + list(v.visit(lib2to3_parse(code))) +``` + +## Black Output + +```py +@dataclass +class DebugVisitor(Visitor[T]): + tree_depth: int = 0 + + def visit_default(self, node: LN) -> Iterator[T]: + indent = ' ' * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) + out(f'{indent}{_type}', fg='yellow') + self.tree_depth += 1 + for child in node.children: + yield from self.visit(child) + + self.tree_depth -= 1 + out(f'{indent}/{_type}', fg='yellow', bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) + out(f'{indent}{_type}', fg='blue', nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. + out(f' {node.prefix!r}', fg='green', bold=False, nl=False) + out(f' {node.value!r}', fg='blue', bold=False) + + @classmethod + def show(cls, code: str) -> None: + """Pretty-prints a given string of `code`. + + Convenience method for debugging. + """ + v: DebugVisitor[None] = DebugVisitor() + list(v.visit(lib2to3_parse(code))) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap new file mode 100644 index 0000000000..020bb3c340 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap @@ -0,0 +1,576 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py +--- +## Input + +```py +# This file doesn't use the standard decomposition. +# Decorator syntax test cases are separated by double # comments. +# Those before the 'output' comment are valid under the old syntax. +# Those after the 'ouput' comment require PEP614 relaxed syntax. +# Do not remove the double # separator before the first test case, it allows +# the comment before the test case to be ignored. + +## + +@decorator +def f(): + ... + +## + +@decorator() +def f(): + ... + +## + +@decorator(arg) +def f(): + ... + +## + +@decorator(kwarg=0) +def f(): + ... + +## + +@decorator(*args) +def f(): + ... + +## + +@decorator(**kwargs) +def f(): + ... + +## + +@decorator(*args, **kwargs) +def f(): + ... + +## + +@decorator(*args, **kwargs,) +def f(): + ... + +## + +@dotted.decorator +def f(): + ... + +## + +@dotted.decorator(arg) +def f(): + ... + +## + +@dotted.decorator(kwarg=0) +def f(): + ... + +## + +@dotted.decorator(*args) +def f(): + ... + +## + +@dotted.decorator(**kwargs) +def f(): + ... + +## + +@dotted.decorator(*args, **kwargs) +def f(): + ... + +## + +@dotted.decorator(*args, **kwargs,) +def f(): + ... + +## + +@double.dotted.decorator +def f(): + ... + +## + +@double.dotted.decorator(arg) +def f(): + ... + +## + +@double.dotted.decorator(kwarg=0) +def f(): + ... + +## + +@double.dotted.decorator(*args) +def f(): + ... + +## + +@double.dotted.decorator(**kwargs) +def f(): + ... + +## + +@double.dotted.decorator(*args, **kwargs) +def f(): + ... + +## + +@double.dotted.decorator(*args, **kwargs,) +def f(): + ... + +## + +@_(sequence["decorator"]) +def f(): + ... + +## + +@eval("sequence['decorator']") +def f(): + ... +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,29 +1,182 @@ ++# This file doesn't use the standard decomposition. ++# Decorator syntax test cases are separated by double # comments. ++# Those before the 'output' comment are valid under the old syntax. ++# Those after the 'ouput' comment require PEP614 relaxed syntax. ++# Do not remove the double # separator before the first test case, it allows ++# the comment before the test case to be ignored. ++ ++## ++ ++@decorator ++def f(): ++ ... ++ ++ ++## ++ ++@decorator() ++def f(): ++ ... ++ ++ ++## ++ ++@decorator(arg) ++def f(): ++ ... ++ ++ ++## ++ ++@decorator(kwarg=0) ++def f(): ++ ... ++ ++ ++## ++ ++@decorator(*NOT_YET_IMPLEMENTED_ExprStarred) ++def f(): ++ ... ++ ++ + ## + +-@decorator()() ++@decorator(**kwargs) + def f(): + ... + ++ + ## + +-@(decorator) ++@decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) + def f(): + ... + ++ + ## + +-@sequence["decorator"] ++@decorator( ++ *NOT_YET_IMPLEMENTED_ExprStarred, ++ **kwargs, ++) + def f(): + ... + ++ + ## + +-@decorator[List[str]] ++@dotted.decorator + def f(): + ... + ++ + ## + +-@var := decorator ++@dotted.decorator(arg) ++def f(): ++ ... ++ ++ ++## ++ ++@dotted.decorator(kwarg=0) ++def f(): ++ ... ++ ++ ++## ++ ++@dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred) ++def f(): ++ ... ++ ++ ++## ++ ++@dotted.decorator(**kwargs) ++def f(): ++ ... ++ ++ ++## ++ ++@dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) ++def f(): ++ ... ++ ++ ++## ++ ++@dotted.decorator( ++ *NOT_YET_IMPLEMENTED_ExprStarred, ++ **kwargs, ++) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator(arg) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator(kwarg=0) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator(**kwargs) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator( ++ *NOT_YET_IMPLEMENTED_ExprStarred, ++ **kwargs, ++) ++def f(): ++ ... ++ ++ ++## ++ ++@_(sequence["decorator"]) ++def f(): ++ ... ++ ++ ++## ++ ++@eval("sequence['decorator']") + def f(): + ... +``` + +## Ruff Output + +```py +# This file doesn't use the standard decomposition. +# Decorator syntax test cases are separated by double # comments. +# Those before the 'output' comment are valid under the old syntax. +# Those after the 'ouput' comment require PEP614 relaxed syntax. +# Do not remove the double # separator before the first test case, it allows +# the comment before the test case to be ignored. + +## + +@decorator +def f(): + ... + + +## + +@decorator() +def f(): + ... + + +## + +@decorator(arg) +def f(): + ... + + +## + +@decorator(kwarg=0) +def f(): + ... + + +## + +@decorator(*NOT_YET_IMPLEMENTED_ExprStarred) +def f(): + ... + + +## + +@decorator(**kwargs) +def f(): + ... + + +## + +@decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) +def f(): + ... + + +## + +@decorator( + *NOT_YET_IMPLEMENTED_ExprStarred, + **kwargs, +) +def f(): + ... + + +## + +@dotted.decorator +def f(): + ... + + +## + +@dotted.decorator(arg) +def f(): + ... + + +## + +@dotted.decorator(kwarg=0) +def f(): + ... + + +## + +@dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred) +def f(): + ... + + +## + +@dotted.decorator(**kwargs) +def f(): + ... + + +## + +@dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) +def f(): + ... + + +## + +@dotted.decorator( + *NOT_YET_IMPLEMENTED_ExprStarred, + **kwargs, +) +def f(): + ... + + +## + +@double.dotted.decorator +def f(): + ... + + +## + +@double.dotted.decorator(arg) +def f(): + ... + + +## + +@double.dotted.decorator(kwarg=0) +def f(): + ... + + +## + +@double.dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred) +def f(): + ... + + +## + +@double.dotted.decorator(**kwargs) +def f(): + ... + + +## + +@double.dotted.decorator(*NOT_YET_IMPLEMENTED_ExprStarred, **kwargs) +def f(): + ... + + +## + +@double.dotted.decorator( + *NOT_YET_IMPLEMENTED_ExprStarred, + **kwargs, +) +def f(): + ... + + +## + +@_(sequence["decorator"]) +def f(): + ... + + +## + +@eval("sequence['decorator']") +def f(): + ... +``` + +## Black Output + +```py +## + +@decorator()() +def f(): + ... + +## + +@(decorator) +def f(): + ... + +## + +@sequence["decorator"] +def f(): + ... + +## + +@decorator[List[str]] +def f(): + ... + +## + +@var := decorator +def f(): + ... +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_no_string_normalization.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_no_string_normalization.py.snap new file mode 100644 index 0000000000..80c7ba60ec --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_no_string_normalization.py.snap @@ -0,0 +1,556 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py +--- +## Input + +```py +class ALonelyClass: + ''' + A multiline class docstring. + ''' + def AnEquallyLonelyMethod(self): + ''' + A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + '''This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not +make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it! + + """ + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): ''' +"hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + ''' + Docstring Docstring Docstring + ''' + pass + + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\ ''' +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,73 +1,75 @@ + class ALonelyClass: +- ''' ++ """ + A multiline class docstring. +- ''' ++ """ + + def AnEquallyLonelyMethod(self): +- ''' +- A multiline method docstring''' ++ """ ++ A multiline method docstring""" + pass + + + def one_function(): +- '''This is a docstring with a single line of text.''' ++ """This is a docstring with a single line of text.""" + pass + + + def shockingly_the_quotes_are_normalized(): +- '''This is a multiline docstring. ++ """This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. +- ''' ++ """ + pass + + + def foo(): +- """This is a docstring with +- some lines of text here +- """ ++ """This is a docstring with ++ some lines of text here ++ """ + return + + + def baz(): + '''"This" is a string with some +- embedded "quotes"''' ++ embedded "quotes"''' + return + + + def poit(): + """ +- Lorem ipsum dolor sit amet. ++ Lorem ipsum dolor sit amet. + +- Consectetur adipiscing elit: +- - sed do eiusmod tempor incididunt ut labore +- - dolore magna aliqua +- - enim ad minim veniam +- - quis nostrud exercitation ullamco laboris nisi +- - aliquip ex ea commodo consequat +- """ ++ Consectetur adipiscing elit: ++ - sed do eiusmod tempor incididunt ut labore ++ - dolore magna aliqua ++ - enim ad minim veniam ++ - quis nostrud exercitation ullamco laboris nisi ++ - aliquip ex ea commodo consequat ++ """ + pass + + + def under_indent(): + """ +- These lines are indented in a way that does not +- make sense. +- """ ++ These lines are indented in a way that does not ++make sense. ++ """ + pass + + + def over_indent(): + """ +- This has a shallow indent +- - But some lines are deeper +- - And the closing quote is too deep ++ This has a shallow indent ++ - But some lines are deeper ++ - And the closing quote is too deep + """ + pass + + + def single_line(): +- """But with a newline after it!""" ++ """But with a newline after it! ++ ++ """ + pass + + +@@ -83,41 +85,41 @@ + + def and_that(): + """ +- "hey yah" """ ++ "hey yah" """ + + + def and_this(): +- ''' +- "hey yah"''' ++ ''' ++ "hey yah"''' + + + def believe_it_or_not_this_is_in_the_py_stdlib(): +- ''' +- "hey yah"''' ++ ''' ++"hey yah"''' + + + def shockingly_the_quotes_are_normalized_v2(): +- ''' ++ """ + Docstring Docstring Docstring +- ''' ++ """ + pass + + + def backslash_space(): +- '\ ' ++ "\ " + + + def multiline_backslash_1(): +- ''' ++ """ + hey\there\ +- \ ''' ++ \ """ + + + def multiline_backslash_2(): +- ''' +- hey there \ ''' ++ """ ++ hey there \ """ + + + def multiline_backslash_3(): +- ''' +- already escaped \\''' ++ """ ++ already escaped \\ """ +``` + +## Ruff Output + +```py +class ALonelyClass: + """ + A multiline class docstring. + """ + + def AnEquallyLonelyMethod(self): + """ + A multiline method docstring""" + pass + + +def one_function(): + """This is a docstring with a single line of text.""" + pass + + +def shockingly_the_quotes_are_normalized(): + """This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + """ + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not +make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it! + + """ + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + ''' +"hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + """ + Docstring Docstring Docstring + """ + pass + + +def backslash_space(): + "\ " + + +def multiline_backslash_1(): + """ + hey\there\ + \ """ + + +def multiline_backslash_2(): + """ + hey there \ """ + + +def multiline_backslash_3(): + """ + already escaped \\ """ +``` + +## Black Output + +```py +class ALonelyClass: + ''' + A multiline class docstring. + ''' + + def AnEquallyLonelyMethod(self): + ''' + A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + '''This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not + make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it!""" + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + ''' + "hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + ''' + Docstring Docstring Docstring + ''' + pass + + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\''' +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_preview_no_string_normalization.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_preview_no_string_normalization.py.snap new file mode 100644 index 0000000000..52233c973b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_preview_no_string_normalization.py.snap @@ -0,0 +1,68 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py +--- +## Input + +```py +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + FR'There was a bug where docstring prefixes would be normalized even with -S.' + + +def do_not_touch_this_prefix3(): + u'''There was a bug where docstring prefixes would be normalized even with -S.''' +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -3,8 +3,8 @@ + + + def do_not_touch_this_prefix2(): +- FR'There was a bug where docstring prefixes would be normalized even with -S.' ++ NOT_YET_IMPLEMENTED_ExprJoinedStr + + + def do_not_touch_this_prefix3(): +- u'''There was a bug where docstring prefixes would be normalized even with -S.''' ++ """There was a bug where docstring prefixes would be normalized even with -S.""" +``` + +## Ruff Output + +```py +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + NOT_YET_IMPLEMENTED_ExprJoinedStr + + +def do_not_touch_this_prefix3(): + """There was a bug where docstring prefixes would be normalized even with -S.""" +``` + +## Black Output + +```py +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + FR'There was a bug where docstring prefixes would be normalized even with -S.' + + +def do_not_touch_this_prefix3(): + u'''There was a bug where docstring prefixes would be normalized even with -S.''' +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap new file mode 100644 index 0000000000..52fbac942f --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap @@ -0,0 +1,220 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py +--- +## Input + +```py +from typing import Union + +@bird +def zoo(): ... + +class A: ... +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg : List[str]) -> None: ... + +class C: ... +@hmm +class D: ... +class E: ... + +@baz +def foo() -> None: + ... + +class F (A , C): ... +def spam() -> None: ... + +@overload +def spam(arg: str) -> str: ... + +var : int = 1 + +def eggs() -> Union[str, int]: ... +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,32 +1,58 @@ +-from typing import Union ++NOT_YET_IMPLEMENTED_StmtImportFrom ++ + + @bird +-def zoo(): ... ++def zoo(): ++ ... ++ ++ ++class A: ++ ... + +-class A: ... + + @bar + class B: +- def BMethod(self) -> None: ... ++ def BMethod(self) -> None: ++ ... ++ + @overload +- def BMethod(self, arg: List[str]) -> None: ... ++ def BMethod(self, arg: List[str]) -> None: ++ ... + +-class C: ... + ++class C: ++ ... ++ ++ + @hmm +-class D: ... ++class D: ++ ... ++ ++ ++class E: ++ ... + +-class E: ... + + @baz +-def foo() -> None: ... ++def foo() -> None: ++ ... ++ ++ ++class F(A, C): ++ ... + +-class F(A, C): ... + +-def spam() -> None: ... ++def spam() -> None: ++ ... ++ ++ + @overload +-def spam(arg: str) -> str: ... ++def spam(arg: str) -> str: ++ ... ++ ++ ++NOT_YET_IMPLEMENTED_StmtAnnAssign + +-var: int = 1 + +-def eggs() -> Union[str, int]: ... ++def eggs() -> Union[str, int]: ++ ... +``` + +## Ruff Output + +```py +NOT_YET_IMPLEMENTED_StmtImportFrom + + +@bird +def zoo(): + ... + + +class A: + ... + + +@bar +class B: + def BMethod(self) -> None: + ... + + @overload + def BMethod(self, arg: List[str]) -> None: + ... + + +class C: + ... + + +@hmm +class D: + ... + + +class E: + ... + + +@baz +def foo() -> None: + ... + + +class F(A, C): + ... + + +def spam() -> None: + ... + + +@overload +def spam(arg: str) -> str: + ... + + +NOT_YET_IMPLEMENTED_StmtAnnAssign + + +def eggs() -> Union[str, int]: + ... +``` + +## Black Output + +```py +from typing import Union + +@bird +def zoo(): ... + +class A: ... + +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg: List[str]) -> None: ... + +class C: ... + +@hmm +class D: ... + +class E: ... + +@baz +def foo() -> None: ... + +class F(A, C): ... + +def spam() -> None: ... +@overload +def spam(arg: str) -> str: ... + +var: int = 1 + +def eggs() -> Union[str, int]: ... +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap new file mode 100644 index 0000000000..e969e1e7a7 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap @@ -0,0 +1,1052 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py +--- +## Input + +```py +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." # Third Comment +) + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) +) + +fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." + +fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." + +comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception." + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( + "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." + % "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." + % ("string", "formatting") +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string." +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", # comment after comma +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), # comment after comma +) + +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, ("short string that should have parens stripped"), z +) + +func_with_bad_parens( + ("short string that should have parens stripped"), + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + ("short string that should have parens stripped"), + z, +) + +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + CONCATENATED + + "using the '+' operator." +) +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal[ + "fakse_literal" +] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + + +x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,6 +1,6 @@ + x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +-x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." ++NOT_YET_IMPLEMENTED_StmtAugAssign + + y = "Short string" + +@@ -12,7 +12,7 @@ + ) + + print( +- "This is a really long string inside of a print statement with no extra arguments attached at the end of it." ++ "This is a really long string inside of a print statement with no extra arguments attached at the end of it.", + ) + + D1 = { +@@ -70,8 +70,8 @@ + bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment +- "case, we should just leave it alone." # Third Comment +-) ++ "case, we should just leave it alone." ++) # Third Comment + + bad_split_func1( + "But what should happen when code has already " +@@ -96,15 +96,13 @@ + ) + + bad_split_func3( +- ( +- "But what should happen when code has already " +- r"been formatted but in the wrong way? Like " +- "with a space at the end instead of the " +- r"beginning. Or what about when it is split too " +- r"soon? In the case of a split that is too " +- "short, black will try to honer the custom " +- "split." +- ), ++ "But what should happen when code has already " ++ r"been formatted but in the wrong way? Like " ++ "with a space at the end instead of the " ++ r"beginning. Or what about when it is split too " ++ r"soon? In the case of a split that is too " ++ "short, black will try to honer the custom " ++ "split.", + xxx, + yyy, + zzz, +@@ -143,9 +141,9 @@ + ) + ) + +-fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." ++fstring = NOT_YET_IMPLEMENTED_ExprJoinedStr + +-fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." ++fstring_with_no_fexprs = NOT_YET_IMPLEMENTED_ExprJoinedStr + + comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. + +@@ -165,30 +163,18 @@ + + triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +-assert ( +- some_type_of_boolean_expression +-), "Followed by a really really really long string that is used to provide context to the AssertionError exception." ++NOT_YET_IMPLEMENTED_StmtAssert + +-assert ( +- some_type_of_boolean_expression +-), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( +- "formatting" +-) ++NOT_YET_IMPLEMENTED_StmtAssert + +-assert some_type_of_boolean_expression, ( +- "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." +- % "formatting" +-) ++NOT_YET_IMPLEMENTED_StmtAssert + +-assert some_type_of_boolean_expression, ( +- "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." +- % ("string", "formatting") +-) ++NOT_YET_IMPLEMENTED_StmtAssert + + some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added +- + " to a variable and then added to another string." ++ + " to a variable and then added to another string.", + ) + + some_function_call( +@@ -212,29 +198,25 @@ + ) + + func_with_bad_comma( +- ( +- "This is a really long string argument to a function that has a trailing comma" +- " which should NOT be there." +- ), ++ "This is a really long string argument to a function that has a trailing comma" ++ " which should NOT be there.", + ) + + func_with_bad_comma( +- ( +- "This is a really long string argument to a function that has a trailing comma" +- " which should NOT be there." +- ), # comment after comma ++ "This is a really long string argument to a function that has a trailing comma" ++ " which should NOT be there.", # comment after comma + ) + + func_with_bad_parens_that_wont_fit_in_one_line( +- ("short string that should have parens stripped"), x, y, z ++ "short string that should have parens stripped", x, y, z + ) + + func_with_bad_parens_that_wont_fit_in_one_line( +- x, y, ("short string that should have parens stripped"), z ++ x, y, "short string that should have parens stripped", z + ) + + func_with_bad_parens( +- ("short string that should have parens stripped"), ++ "short string that should have parens stripped", + x, + y, + z, +@@ -243,21 +225,13 @@ + func_with_bad_parens( + x, + y, +- ("short string that should have parens stripped"), ++ "short string that should have parens stripped", + z, + ) + +-annotated_variable: Final = ( +- "This is a large " +- + STRING +- + " that has been " +- + CONCATENATED +- + "using the '+' operator." +-) +-annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +-annotated_variable: Literal[ +- "fakse_literal" +-] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." ++NOT_YET_IMPLEMENTED_StmtAnnAssign ++NOT_YET_IMPLEMENTED_StmtAnnAssign ++NOT_YET_IMPLEMENTED_StmtAnnAssign + + backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" + backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +@@ -271,10 +245,10 @@ + + + def foo(): +- yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." ++ NOT_YET_IMPLEMENTED_ExprYield + + +-x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." ++x = NOT_YET_IMPLEMENTED_ExprJoinedStr + + long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore +``` + +## Ruff Output + +```py +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +NOT_YET_IMPLEMENTED_StmtAugAssign + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it.", +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." +) # Third Comment + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) +) + +fstring = NOT_YET_IMPLEMENTED_ExprJoinedStr + +fstring_with_no_fexprs = NOT_YET_IMPLEMENTED_ExprJoinedStr + +comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +NOT_YET_IMPLEMENTED_StmtAssert + +NOT_YET_IMPLEMENTED_StmtAssert + +NOT_YET_IMPLEMENTED_StmtAssert + +NOT_YET_IMPLEMENTED_StmtAssert + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string.", +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", # comment after comma +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there.", +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there.", # comment after comma +) + +func_with_bad_parens_that_wont_fit_in_one_line( + "short string that should have parens stripped", x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, "short string that should have parens stripped", z +) + +func_with_bad_parens( + "short string that should have parens stripped", + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + "short string that should have parens stripped", + z, +) + +NOT_YET_IMPLEMENTED_StmtAnnAssign +NOT_YET_IMPLEMENTED_StmtAnnAssign +NOT_YET_IMPLEMENTED_StmtAnnAssign + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + NOT_YET_IMPLEMENTED_ExprYield + + +x = NOT_YET_IMPLEMENTED_ExprJoinedStr + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) +``` + +## Black Output + +```py +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." # Third Comment +) + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) +) + +fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." + +fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." + +comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception." + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( + "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." + % "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." + % ("string", "formatting") +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string." +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", # comment after comma +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), # comment after comma +) + +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, ("short string that should have parens stripped"), z +) + +func_with_bad_parens( + ("short string that should have parens stripped"), + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + ("short string that should have parens stripped"), + z, +) + +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + CONCATENATED + + "using the '+' operator." +) +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal[ + "fakse_literal" +] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + + +x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__power_op_newline.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__power_op_newline.py.snap new file mode 100644 index 0000000000..991e030ac9 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__power_op_newline.py.snap @@ -0,0 +1,44 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py +--- +## Input + +```py +importA;()<<0**0# +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,6 +1,2 @@ + importA +-( +- () +- << 0 +- ** 0 +-) # ++() << 0**0 # +``` + +## Ruff Output + +```py +importA +() << 0**0 # +``` + +## Black Output + +```py +importA +( + () + << 0 + ** 0 +) # +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap new file mode 100644 index 0000000000..9fa5227c0a --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap @@ -0,0 +1,244 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py +--- +## Input + +```py +'''''' +'\'' +'"' +"'" +"\"" +"Hello" +"Don't do that" +'Here is a "' +'What\'s the deal here?' +"What's the deal \"here\"?" +"And \"here\"?" +"""Strings with "" in them""" +'''Strings with "" in them''' +'''Here's a "''' +'''Here's a " ''' +'''Just a normal triple +quote''' +f"just a normal {f} string" +f'''This is a triple-quoted {f}-string''' +f'MOAR {" ".join([])}' +f"MOAR {' '.join([])}" +r"raw string ftw" +r'Date d\'expiration:(.*)' +r'Tricky "quote' +r'Not-so-tricky \"quote' +rf'{yay}' +'\n\ +The \"quick\"\n\ +brown fox\n\ +jumps over\n\ +the \'lazy\' dog.\n\ +' +re.compile(r'[\\"]') +"x = ''; y = \"\"" +"x = '''; y = \"\"" +"x = ''''; y = \"\"" +"x = '' ''; y = \"\"" +"x = ''; y = \"\"\"" +"x = '''; y = \"\"\"\"" +"x = ''''; y = \"\"\"\"\"" +"x = '' ''; y = \"\"\"\"\"" +'unnecessary \"\"escaping' +"unnecessary \'\'escaping" +'\\""' +"\\''" +'Lots of \\\\\\\\\'quotes\'' +f'{y * " "} \'{z}\'' +f'{{y * " "}} \'{z}\'' +f'\'{z}\' {y * " "}' +f'{y * x} \'{z}\'' +'\'{z}\' {y * " "}' +'{y * x} \'{z}\'' + +# We must bail out if changing the quotes would introduce backslashes in f-string +# expressions. xref: https://github.com/psf/black/issues/2348 +f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +f"\"{a}\"{'hello' * b}\"{c}\"" +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -15,16 +15,21 @@ + """Here's a " """ + """Just a normal triple + quote""" +-f"just a normal {f} string" +-f"""This is a triple-quoted {f}-string""" +-f'MOAR {" ".join([])}' +-f"MOAR {' '.join([])}" ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++NOT_YET_IMPLEMENTED_ExprJoinedStr + r"raw string ftw" +-r"Date d\'expiration:(.*)" ++r"Date d'expiration:(.*)" + r'Tricky "quote' +-r"Not-so-tricky \"quote" +-rf"{yay}" +-"\nThe \"quick\"\nbrown fox\njumps over\nthe 'lazy' dog.\n" ++r'Not-so-tricky "quote' ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++"\n\ ++The \"quick\"\n\ ++brown fox\n\ ++jumps over\n\ ++the 'lazy' dog.\n\ ++" + re.compile(r'[\\"]') + "x = ''; y = \"\"" + "x = '''; y = \"\"" +@@ -39,14 +44,14 @@ + '\\""' + "\\''" + "Lots of \\\\\\\\'quotes'" +-f'{y * " "} \'{z}\'' +-f"{{y * \" \"}} '{z}'" +-f'\'{z}\' {y * " "}' +-f"{y * x} '{z}'" ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++NOT_YET_IMPLEMENTED_ExprJoinedStr + "'{z}' {y * \" \"}" + "{y * x} '{z}'" + + # We must bail out if changing the quotes would introduce backslashes in f-string + # expressions. xref: https://github.com/psf/black/issues/2348 +-f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +-f"\"{a}\"{'hello' * b}\"{c}\"" ++NOT_YET_IMPLEMENTED_ExprJoinedStr ++NOT_YET_IMPLEMENTED_ExprJoinedStr +``` + +## Ruff Output + +```py +"""""" +"'" +'"' +"'" +'"' +"Hello" +"Don't do that" +'Here is a "' +"What's the deal here?" +'What\'s the deal "here"?' +'And "here"?' +"""Strings with "" in them""" +"""Strings with "" in them""" +'''Here's a "''' +"""Here's a " """ +"""Just a normal triple +quote""" +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +r"raw string ftw" +r"Date d'expiration:(.*)" +r'Tricky "quote' +r'Not-so-tricky "quote' +NOT_YET_IMPLEMENTED_ExprJoinedStr +"\n\ +The \"quick\"\n\ +brown fox\n\ +jumps over\n\ +the 'lazy' dog.\n\ +" +re.compile(r'[\\"]') +"x = ''; y = \"\"" +"x = '''; y = \"\"" +"x = ''''; y = \"\"" +"x = '' ''; y = \"\"" +'x = \'\'; y = """' +'x = \'\'\'; y = """"' +'x = \'\'\'\'; y = """""' +'x = \'\' \'\'; y = """""' +'unnecessary ""escaping' +"unnecessary ''escaping" +'\\""' +"\\''" +"Lots of \\\\\\\\'quotes'" +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +"'{z}' {y * \" \"}" +"{y * x} '{z}'" + +# We must bail out if changing the quotes would introduce backslashes in f-string +# expressions. xref: https://github.com/psf/black/issues/2348 +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +``` + +## Black Output + +```py +"""""" +"'" +'"' +"'" +'"' +"Hello" +"Don't do that" +'Here is a "' +"What's the deal here?" +'What\'s the deal "here"?' +'And "here"?' +"""Strings with "" in them""" +"""Strings with "" in them""" +'''Here's a "''' +"""Here's a " """ +"""Just a normal triple +quote""" +f"just a normal {f} string" +f"""This is a triple-quoted {f}-string""" +f'MOAR {" ".join([])}' +f"MOAR {' '.join([])}" +r"raw string ftw" +r"Date d\'expiration:(.*)" +r'Tricky "quote' +r"Not-so-tricky \"quote" +rf"{yay}" +"\nThe \"quick\"\nbrown fox\njumps over\nthe 'lazy' dog.\n" +re.compile(r'[\\"]') +"x = ''; y = \"\"" +"x = '''; y = \"\"" +"x = ''''; y = \"\"" +"x = '' ''; y = \"\"" +'x = \'\'; y = """' +'x = \'\'\'; y = """"' +'x = \'\'\'\'; y = """""' +'x = \'\' \'\'; y = """""' +'unnecessary ""escaping' +"unnecessary ''escaping" +'\\""' +"\\''" +"Lots of \\\\\\\\'quotes'" +f'{y * " "} \'{z}\'' +f"{{y * \" \"}} '{z}'" +f'\'{z}\' {y * " "}' +f"{y * x} '{z}'" +"'{z}' {y * \" \"}" +"{y * x} '{z}'" + +# We must bail out if changing the quotes would introduce backslashes in f-string +# expressions. xref: https://github.com/psf/black/issues/2348 +f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +f"\"{a}\"{'hello' * b}\"{c}\"" +``` + + 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 new file mode 100644 index 0000000000..ce5a833fd2 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_complex.py.snap @@ -0,0 +1,549 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py +--- +## Input + +```py +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +match x: + case -0j: + y = 0 +# case black_test_patma_142 +match x: + case bytes(z): + y = 0 +# case black_test_patma_073 +match x: + case 0 if 0: + y = 0 + case 0 if 1: + y = 1 +# case black_test_patma_006 +match 3: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_049 +match x: + case [0, 1] | [1, 0]: + y = 0 +# case black_check_sequence_then_mapping +match x: + case [*_]: + return "seq" + case {}: + return "map" +# case black_test_patma_035 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +# case black_test_patma_107 +match x: + case 0.25 + 1.75j: + y = 0 +# case black_test_patma_097 +match x: + case -0j: + y = 0 +# case black_test_patma_007 +match 4: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_154 +match x: + case 0 if x: + y = 0 +# case black_test_patma_134 +match x: + case {1: 0}: + y = 0 + case {0: 0}: + y = 1 + case {**z}: + y = 2 +# case black_test_patma_185 +match Seq(): + case [*_]: + y = 0 +# case black_test_patma_063 +match x: + case 1: + y = 0 + case 1: + y = 1 +# case black_test_patma_248 +match x: + case {"foo": bar}: + y = bar +# case black_test_patma_019 +match (0, 1, 2): + case [0, 1, *x, 2]: + y = 0 +# case black_test_patma_052 +match x: + case [0]: + y = 0 + case [1, 0] if (x := x[:0]): + y = 1 + case [1, 0]: + y = 2 +# case black_test_patma_191 +match w: + case [x, y, *_]: + z = 0 +# case black_test_patma_110 +match x: + case -0.25 - 1.75j: + y = 0 +# case black_test_patma_151 +match (x,): + case [y]: + z = 0 +# case black_test_patma_114 +match x: + case A.B.C.D: + y = 0 +# case black_test_patma_232 +match x: + case None: + y = 0 +# case black_test_patma_058 +match x: + case 0: + y = 0 +# case black_test_patma_233 +match x: + case False: + y = 0 +# case black_test_patma_078 +match x: + case []: + y = 0 + case [""]: + y = 1 + case "": + y = 2 +# case black_test_patma_156 +match x: + case z: + y = 0 +# case black_test_patma_189 +match w: + case [x, y, *rest]: + z = 0 +# case black_test_patma_042 +match x: + case (0 as z) | (1 as z) | (2 as z) if z == x % 2: + y = 0 +# case black_test_patma_034 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,144 +1,60 @@ + # Cases sampled from Lib/test/test_patma.py + + # case black_test_patma_098 +-match x: +- case -0j: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_142 +-match x: +- case bytes(z): +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_073 +-match x: +- case 0 if 0: +- y = 0 +- case 0 if 1: +- y = 1 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_006 +-match 3: +- case 0 | 1 | 2 | 3: +- x = True ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_049 +-match x: +- case [0, 1] | [1, 0]: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_check_sequence_then_mapping +-match x: +- case [*_]: +- return "seq" +- case {}: +- return "map" ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_035 +-match x: +- case {0: [1, 2, {}]}: +- y = 0 +- case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: +- y = 1 +- case []: +- y = 2 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_107 +-match x: +- case 0.25 + 1.75j: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_097 +-match x: +- case -0j: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_007 +-match 4: +- case 0 | 1 | 2 | 3: +- x = True ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_154 +-match x: +- case 0 if x: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_134 +-match x: +- case {1: 0}: +- y = 0 +- case {0: 0}: +- y = 1 +- case {**z}: +- y = 2 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_185 +-match Seq(): +- case [*_]: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_063 +-match x: +- case 1: +- y = 0 +- case 1: +- y = 1 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_248 +-match x: +- case {"foo": bar}: +- y = bar ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_019 +-match (0, 1, 2): +- case [0, 1, *x, 2]: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_052 +-match x: +- case [0]: +- y = 0 +- case [1, 0] if (x := x[:0]): +- y = 1 +- case [1, 0]: +- y = 2 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_191 +-match w: +- case [x, y, *_]: +- z = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_110 +-match x: +- case -0.25 - 1.75j: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_151 +-match (x,): +- case [y]: +- z = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_114 +-match x: +- case A.B.C.D: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_232 +-match x: +- case None: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_058 +-match x: +- case 0: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_233 +-match x: +- case False: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_078 +-match x: +- case []: +- y = 0 +- case [""]: +- y = 1 +- case "": +- y = 2 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_156 +-match x: +- case z: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_189 +-match w: +- case [x, y, *rest]: +- z = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_042 +-match x: +- case (0 as z) | (1 as z) | (2 as z) if z == x % 2: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_034 +-match x: +- case {0: [1, 2, {}]}: +- y = 0 +- case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: +- y = 1 +- case []: +- y = 2 ++NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Ruff Output + +```py +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_142 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_073 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_006 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_049 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_check_sequence_then_mapping +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_035 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_107 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_097 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_007 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_154 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_134 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_185 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_063 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_248 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_019 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_052 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_191 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_110 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_151 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_114 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_232 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_058 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_233 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_078 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_156 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_189 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_042 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_034 +NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Black Output + +```py +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +match x: + case -0j: + y = 0 +# case black_test_patma_142 +match x: + case bytes(z): + y = 0 +# case black_test_patma_073 +match x: + case 0 if 0: + y = 0 + case 0 if 1: + y = 1 +# case black_test_patma_006 +match 3: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_049 +match x: + case [0, 1] | [1, 0]: + y = 0 +# case black_check_sequence_then_mapping +match x: + case [*_]: + return "seq" + case {}: + return "map" +# case black_test_patma_035 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +# case black_test_patma_107 +match x: + case 0.25 + 1.75j: + y = 0 +# case black_test_patma_097 +match x: + case -0j: + y = 0 +# case black_test_patma_007 +match 4: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_154 +match x: + case 0 if x: + y = 0 +# case black_test_patma_134 +match x: + case {1: 0}: + y = 0 + case {0: 0}: + y = 1 + case {**z}: + y = 2 +# case black_test_patma_185 +match Seq(): + case [*_]: + y = 0 +# case black_test_patma_063 +match x: + case 1: + y = 0 + case 1: + y = 1 +# case black_test_patma_248 +match x: + case {"foo": bar}: + y = bar +# case black_test_patma_019 +match (0, 1, 2): + case [0, 1, *x, 2]: + y = 0 +# case black_test_patma_052 +match x: + case [0]: + y = 0 + case [1, 0] if (x := x[:0]): + y = 1 + case [1, 0]: + y = 2 +# case black_test_patma_191 +match w: + case [x, y, *_]: + z = 0 +# case black_test_patma_110 +match x: + case -0.25 - 1.75j: + y = 0 +# case black_test_patma_151 +match (x,): + case [y]: + z = 0 +# case black_test_patma_114 +match x: + case A.B.C.D: + y = 0 +# case black_test_patma_232 +match x: + case None: + y = 0 +# case black_test_patma_058 +match x: + case 0: + y = 0 +# case black_test_patma_233 +match x: + case False: + y = 0 +# case black_test_patma_078 +match x: + case []: + y = 0 + case [""]: + y = 1 + case "": + y = 2 +# case black_test_patma_156 +match x: + case z: + y = 0 +# case black_test_patma_189 +match w: + case [x, y, *rest]: + z = 0 +# case black_test_patma_042 +match x: + case (0 as z) | (1 as z) | (2 as z) if z == x % 2: + y = 0 +# case black_test_patma_034 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +``` + + 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 new file mode 100644 index 0000000000..9afb2c247d --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap @@ -0,0 +1,443 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py +--- +## Input + +```py +import match + +match something: + case [a as b]: + print(b) + case [a as b, c, d, e as f]: + print(f) + case Point(a as b): + print(b) + case Point(int() as x, int() as y): + print(x, y) + + +match = 1 +case: int = re.match(something) + +match re.match(case): + case type("match", match): + pass + case match: + pass + + +def func(match: case, case: match) -> case: + match Something(): + case func(match, case): + ... + case another: + ... + + +match maybe, multiple: + case perhaps, 5: + pass + case perhaps, 6,: + pass + + +match more := (than, one), indeed,: + case _, (5, 6): + pass + case [[5], (6)], [7],: + pass + case _: + pass + + +match a, *b, c: + case [*_]: + assert "seq" == _ + case {}: + assert "map" == b + + +match match( + case, + match( + match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match + ), + case, +): + case case( + match=case, + case=re.match( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + ), + ): + pass + + case [a as match]: + pass + + case case: + pass + + +match match: + case case: + pass + + +match a, *b(), c: + case d, *f, g: + pass + + +match something: + case { + "key": key as key_1, + "password": PASS.ONE | PASS.TWO | PASS.THREE as password, + }: + pass + case {"maybe": something(complicated as this) as that}: + pass + + +match something: + case 1 as a: + pass + + case 2 as b, 3 as c: + pass + + case 4 as d, (5 as e), (6 | 7 as g), *h: + pass + + +match bar1: + case Foo(aa=Callable() as aa, bb=int()): + 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 + ): + pass +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,119 +1,43 @@ +-import match ++NOT_YET_IMPLEMENTED_StmtImport + +-match something: +- case [a as b]: +- print(b) +- case [a as b, c, d, e as f]: +- print(f) +- case Point(a as b): +- print(b) +- case Point(int() as x, int() as y): +- print(x, y) ++NOT_YET_IMPLEMENTED_StmtMatch + + + match = 1 +-case: int = re.match(something) ++NOT_YET_IMPLEMENTED_StmtAnnAssign + +-match re.match(case): +- case type("match", match): +- pass +- case match: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + + def func(match: case, case: match) -> case: +- match Something(): +- case func(match, case): +- ... +- case another: +- ... ++ NOT_YET_IMPLEMENTED_StmtMatch + + +-match maybe, multiple: +- case perhaps, 5: +- pass +- case perhaps, 6,: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match more := (than, one), indeed,: +- case _, (5, 6): +- pass +- case [[5], (6)], [7],: +- pass +- case _: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match a, *b, c: +- case [*_]: +- assert "seq" == _ +- case {}: +- assert "map" == b ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match match( +- case, +- match( +- match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match +- ), +- case, +-): +- case case( +- match=case, +- case=re.match( +- loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong +- ), +- ): +- pass +- +- case [a as match]: +- pass +- +- case case: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match match: +- case case: +- pass +- +- +-match a, *b(), c: +- case d, *f, g: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match something: +- case { +- "key": key as key_1, +- "password": PASS.ONE | PASS.TWO | PASS.THREE as password, +- }: +- pass +- case {"maybe": something(complicated as this) as that}: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match something: +- case 1 as a: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + +- case 2 as b, 3 as c: +- pass + +- case 4 as d, (5 as e), (6 | 7 as g), *h: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match bar1: +- case Foo(aa=Callable() as aa, bb=int()): +- print(bar1.aa, bar1.bb) +- case _: +- print("no match", "\n") ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match bar1: +- case Foo( +- normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u +- ): +- pass ++NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Ruff Output + +```py +NOT_YET_IMPLEMENTED_StmtImport + +NOT_YET_IMPLEMENTED_StmtMatch + + +match = 1 +NOT_YET_IMPLEMENTED_StmtAnnAssign + +NOT_YET_IMPLEMENTED_StmtMatch + + +def func(match: case, case: match) -> case: + NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Black Output + +```py +import match + +match something: + case [a as b]: + print(b) + case [a as b, c, d, e as f]: + print(f) + case Point(a as b): + print(b) + case Point(int() as x, int() as y): + print(x, y) + + +match = 1 +case: int = re.match(something) + +match re.match(case): + case type("match", match): + pass + case match: + pass + + +def func(match: case, case: match) -> case: + match Something(): + case func(match, case): + ... + case another: + ... + + +match maybe, multiple: + case perhaps, 5: + pass + case perhaps, 6,: + pass + + +match more := (than, one), indeed,: + case _, (5, 6): + pass + case [[5], (6)], [7],: + pass + case _: + pass + + +match a, *b, c: + case [*_]: + assert "seq" == _ + case {}: + assert "map" == b + + +match match( + case, + match( + match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match + ), + case, +): + case case( + match=case, + case=re.match( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + ), + ): + pass + + case [a as match]: + pass + + case case: + pass + + +match match: + case case: + pass + + +match a, *b(), c: + case d, *f, g: + pass + + +match something: + case { + "key": key as key_1, + "password": PASS.ONE | PASS.TWO | PASS.THREE as password, + }: + pass + case {"maybe": something(complicated as this) as that}: + pass + + +match something: + case 1 as a: + pass + + case 2 as b, 3 as c: + pass + + case 4 as d, (5 as e), (6 | 7 as g), *h: + pass + + +match bar1: + case Foo(aa=Callable() as aa, bb=int()): + 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 + ): + 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 new file mode 100644 index 0000000000..e1aef4ad1b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap @@ -0,0 +1,425 @@ +--- +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 +@@ -1,12 +1,12 @@ + re.match() + match = a + with match() as match: +- match = f"{match}" ++ match = NOT_YET_IMPLEMENTED_ExprJoinedStr + + re.match() + match = a + with match() as match: +- match = f"{match}" ++ match = NOT_YET_IMPLEMENTED_ExprJoinedStr + + + def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: +@@ -23,13 +23,9 @@ + pygram.python_grammar, + ] + +- match match: +- case case: +- match match: +- case case: +- pass ++ NOT_YET_IMPLEMENTED_StmtMatch + +- if all(version.is_python2() for version in target_versions): ++ if all((i for i in [])): + # Python 2-only code, so try Python 2 grammars. + return [ + # Python 2.7 with future print_function import +@@ -41,13 +37,11 @@ + re.match() + match = a + with match() as match: +- match = f"{match}" ++ match = NOT_YET_IMPLEMENTED_ExprJoinedStr + + def test_patma_139(self): + x = False +- match x: +- case bool(z): +- y = 0 ++ NOT_YET_IMPLEMENTED_StmtMatch + self.assertIs(x, False) + self.assertEqual(y, 0) + self.assertIs(z, x) +@@ -72,16 +66,12 @@ + def test_patma_155(self): + x = 0 + y = None +- match x: +- case 1e1000: +- y = 0 ++ NOT_YET_IMPLEMENTED_StmtMatch + self.assertEqual(x, 0) + self.assertIs(y, None) + + x = range(3) +- match x: +- case [y, case as x, z]: +- w = 0 ++ NOT_YET_IMPLEMENTED_StmtMatch + + # At least one of the above branches must have been taken, because every Python + # version has exactly one of the two 'ASYNC_*' flags +@@ -91,7 +81,7 @@ + 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" ++ NOT_YET_IMPLEMENTED_StmtAugAssign + + grammars = get_grammars(set(target_versions)) + +@@ -99,9 +89,9 @@ + re.match() + match = a + with match() as match: +- match = f"{match}" ++ match = NOT_YET_IMPLEMENTED_ExprJoinedStr + + re.match() + match = a + with match() as match: +- match = f"{match}" ++ match = NOT_YET_IMPLEMENTED_ExprJoinedStr +``` + +## Ruff Output + +```py +re.match() +match = a +with match() as match: + match = NOT_YET_IMPLEMENTED_ExprJoinedStr + +re.match() +match = a +with match() as match: + match = NOT_YET_IMPLEMENTED_ExprJoinedStr + + +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, + ] + + NOT_YET_IMPLEMENTED_StmtMatch + + if all((i for i in [])): + # 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 = NOT_YET_IMPLEMENTED_ExprJoinedStr + + def test_patma_139(self): + x = False + NOT_YET_IMPLEMENTED_StmtMatch + 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 + NOT_YET_IMPLEMENTED_StmtMatch + self.assertEqual(x, 0) + self.assertIs(y, None) + + x = range(3) + NOT_YET_IMPLEMENTED_StmtMatch + + # 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"): + NOT_YET_IMPLEMENTED_StmtAugAssign + + grammars = get_grammars(set(target_versions)) + + +re.match() +match = a +with match() as match: + match = NOT_YET_IMPLEMENTED_ExprJoinedStr + +re.match() +match = a +with match() as match: + match = NOT_YET_IMPLEMENTED_ExprJoinedStr +``` + +## 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 new file mode 100644 index 0000000000..a165da5f84 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_simple.py.snap @@ -0,0 +1,343 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py +--- +## Input + +```py +# Cases sampled from PEP 636 examples + +match command.split(): + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case [action]: + ... # interpret single-verb action + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case ["quit"]: + print("Goodbye!") + quit_game() + case ["look"]: + current_room.describe() + case ["get", obj]: + character.get(obj, current_room) + case ["go", direction]: + current_room = current_room.neighbor(direction) + # The rest of your commands go here + +match command.split(): + case ["drop", *objects]: + for obj in objects: + character.drop(obj, current_room) + # The rest of your commands go here + +match command.split(): + case ["quit"]: + pass + case ["go", direction]: + print("Going:", direction) + case ["drop", *objects]: + print("Dropping: ", *objects) + case _: + print(f"Sorry, I couldn't understand {command!r}") + +match command.split(): + case ["north"] | ["go", "north"]: + current_room = current_room.neighbor("north") + case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: + ... # Code for picking up the given object + +match command.split(): + case ["go", ("north" | "south" | "east" | "west")]: + current_room = current_room.neighbor(...) + # how do I know which direction to go? + +match command.split(): + case ["go", ("north" | "south" | "east" | "west") as direction]: + current_room = current_room.neighbor(direction) + +match command.split(): + case ["go", direction] if direction in current_room.exits: + current_room = current_room.neighbor(direction) + case ["go", _]: + print("Sorry, you can't go that way") + +match event.get(): + case Click(position=(x, y)): + handle_click_at(x, y) + case KeyPress(key_name="Q") | Quit(): + game.quit() + case KeyPress(key_name="up arrow"): + game.go_north() + case KeyPress(): + 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 + handle_click_at(x, y) + case Click(): + pass # ignore other clicks + + +def where_is(point): + match point: + case Point(x=0, y=0): + print("Origin") + case Point(x=0, y=y): + print(f"Y={y}") + case Point(x=x, y=0): + print(f"X={x}") + case Point(): + print("Somewhere else") + case _: + print("Not a point") +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,92 +1,27 @@ + # Cases sampled from PEP 636 examples + +-match command.split(): +- case [action, obj]: +- ... # interpret action, obj ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case [action]: +- ... # interpret single-verb action +- case [action, obj]: +- ... # interpret action, obj ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["quit"]: +- print("Goodbye!") +- quit_game() +- case ["look"]: +- current_room.describe() +- case ["get", obj]: +- character.get(obj, current_room) +- case ["go", direction]: +- current_room = current_room.neighbor(direction) +- # The rest of your commands go here ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["drop", *objects]: +- for obj in objects: +- character.drop(obj, current_room) +- # The rest of your commands go here ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["quit"]: +- pass +- case ["go", direction]: +- print("Going:", direction) +- case ["drop", *objects]: +- print("Dropping: ", *objects) +- case _: +- print(f"Sorry, I couldn't understand {command!r}") ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["north"] | ["go", "north"]: +- current_room = current_room.neighbor("north") +- case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: +- ... # Code for picking up the given object ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["go", ("north" | "south" | "east" | "west")]: +- current_room = current_room.neighbor(...) +- # how do I know which direction to go? ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["go", ("north" | "south" | "east" | "west") as direction]: +- current_room = current_room.neighbor(direction) ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["go", direction] if direction in current_room.exits: +- current_room = current_room.neighbor(direction) +- case ["go", _]: +- print("Sorry, you can't go that way") ++NOT_YET_IMPLEMENTED_StmtMatch + +-match event.get(): +- case Click(position=(x, y)): +- handle_click_at(x, y) +- case KeyPress(key_name="Q") | Quit(): +- game.quit() +- case KeyPress(key_name="up arrow"): +- game.go_north() +- case KeyPress(): +- pass # Ignore other keystrokes +- case other_event: +- raise ValueError(f"Unrecognized event: {other_event}") ++NOT_YET_IMPLEMENTED_StmtMatch + +-match event.get(): +- case Click((x, y), button=Button.LEFT): # This is a left click +- handle_click_at(x, y) +- case Click(): +- pass # ignore other clicks ++NOT_YET_IMPLEMENTED_StmtMatch + + + def where_is(point): +- match point: +- case Point(x=0, y=0): +- print("Origin") +- case Point(x=0, y=y): +- print(f"Y={y}") +- case Point(x=x, y=0): +- print(f"X={x}") +- case Point(): +- print("Somewhere else") +- case _: +- print("Not a point") ++ NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Ruff Output + +```py +# Cases sampled from PEP 636 examples + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + + +def where_is(point): + NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Black Output + +```py +# Cases sampled from PEP 636 examples + +match command.split(): + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case [action]: + ... # interpret single-verb action + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case ["quit"]: + print("Goodbye!") + quit_game() + case ["look"]: + current_room.describe() + case ["get", obj]: + character.get(obj, current_room) + case ["go", direction]: + current_room = current_room.neighbor(direction) + # The rest of your commands go here + +match command.split(): + case ["drop", *objects]: + for obj in objects: + character.drop(obj, current_room) + # The rest of your commands go here + +match command.split(): + case ["quit"]: + pass + case ["go", direction]: + print("Going:", direction) + case ["drop", *objects]: + print("Dropping: ", *objects) + case _: + print(f"Sorry, I couldn't understand {command!r}") + +match command.split(): + case ["north"] | ["go", "north"]: + current_room = current_room.neighbor("north") + case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: + ... # Code for picking up the given object + +match command.split(): + case ["go", ("north" | "south" | "east" | "west")]: + current_room = current_room.neighbor(...) + # how do I know which direction to go? + +match command.split(): + case ["go", ("north" | "south" | "east" | "west") as direction]: + current_room = current_room.neighbor(direction) + +match command.split(): + case ["go", direction] if direction in current_room.exits: + current_room = current_room.neighbor(direction) + case ["go", _]: + print("Sorry, you can't go that way") + +match event.get(): + case Click(position=(x, y)): + handle_click_at(x, y) + case KeyPress(key_name="Q") | Quit(): + game.quit() + case KeyPress(key_name="up arrow"): + game.go_north() + case KeyPress(): + 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 + handle_click_at(x, y) + case Click(): + pass # ignore other clicks + + +def where_is(point): + match point: + case Point(x=0, y=0): + print("Origin") + case Point(x=0, y=y): + print(f"Y={y}") + case Point(x=x, y=0): + print(f"X={x}") + 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 new file mode 100644 index 0000000000..6a8135be42 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap @@ -0,0 +1,186 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py +--- +## Input + +```py +match something: + case b(): print(1+1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=- 1 + ): print(1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, + ): print(2) + case a: pass + +match( + arg # comment +) + +match( +) + +match( + + +) + +case( + arg # comment +) + +case( +) + +case( + + +) + + +re.match( + something # fast +) +re.match( + + + +) +match match( + + +): + case case( + arg, # comment + ): + pass +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,35 +1,24 @@ +-match something: +- case b(): +- print(1 + 1) +- case c( +- very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 +- ): +- print(1) +- case c( +- very_complex=True, +- perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, +- ): +- print(2) +- case a: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + +-match(arg) # comment ++match( ++ arg, # comment ++) + + match() + + match() + +-case(arg) # comment ++case( ++ arg, # comment ++) + + case() + + case() + + +-re.match(something) # fast ++re.match( ++ something, # fast ++) + re.match() +-match match(): +- case case( +- arg, # comment +- ): +- pass ++NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Ruff Output + +```py +NOT_YET_IMPLEMENTED_StmtMatch + +match( + arg, # comment +) + +match() + +match() + +case( + arg, # comment +) + +case() + +case() + + +re.match( + something, # fast +) +re.match() +NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Black Output + +```py +match something: + case b(): + print(1 + 1) + case c( + very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 + ): + print(1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, + ): + print(2) + case a: + pass + +match(arg) # comment + +match() + +match() + +case(arg) # comment + +case() + +case() + + +re.match(something) # fast +re.match() +match match(): + case case( + arg, # comment + ): + pass +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap new file mode 100644 index 0000000000..64f4277f8b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap @@ -0,0 +1,96 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py +--- +## Input + +```py +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[a:=0] +x[a:=0, b:=1] +x[5, b:=0] + +# Walruses are allowed inside generator expressions on function calls since 3.10. +if any(match := pattern_error.match(s) for s in buffer): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +f(a := b + c for c in range(10)) +f((a := b + c for c in range(10)), x) +f(y=(a := b + c for c in range(10))) +f(x, (a := b + c for c in range(10)), y=z, **q) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,15 +1,15 @@ + # Unparenthesized walruses are now allowed in indices since Python 3.10. +-x[a:=0] +-x[a:=0, b:=1] +-x[5, b:=0] ++x[NOT_YET_IMPLEMENTED_ExprNamedExpr] ++x[NOT_YET_IMPLEMENTED_ExprNamedExpr, NOT_YET_IMPLEMENTED_ExprNamedExpr] ++x[5, NOT_YET_IMPLEMENTED_ExprNamedExpr] + + # Walruses are allowed inside generator expressions on function calls since 3.10. +-if any(match := pattern_error.match(s) for s in buffer): ++if any((i for i in [])): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +-f(a := b + c for c in range(10)) +-f((a := b + c for c in range(10)), x) +-f(y=(a := b + c for c in range(10))) +-f(x, (a := b + c for c in range(10)), y=z, **q) ++f((i for i in [])) ++f((i for i in []), x) ++f(y=(i for i in [])) ++f(x, (i for i in []), y=z, **q) +``` + +## Ruff Output + +```py +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[NOT_YET_IMPLEMENTED_ExprNamedExpr] +x[NOT_YET_IMPLEMENTED_ExprNamedExpr, NOT_YET_IMPLEMENTED_ExprNamedExpr] +x[5, NOT_YET_IMPLEMENTED_ExprNamedExpr] + +# Walruses are allowed inside generator expressions on function calls since 3.10. +if any((i for i in [])): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +f((i for i in [])) +f((i for i in []), x) +f(y=(i for i in [])) +f(x, (i for i in []), y=z, **q) +``` + +## Black Output + +```py +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[a:=0] +x[a:=0, b:=1] +x[5, b:=0] + +# Walruses are allowed inside generator expressions on function calls since 3.10. +if any(match := pattern_error.match(s) for s in buffer): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +f(a := b + c for c in range(10)) +f((a := b + c for c in range(10)), x) +f(y=(a := b + c for c in range(10))) +f(x, (a := b + c for c in range(10)), y=z, **q) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__remove_newline_after_match.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__remove_newline_after_match.py.snap new file mode 100644 index 0000000000..6099825e88 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__remove_newline_after_match.py.snap @@ -0,0 +1,76 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py +--- +## Input + +```py +def http_status(status): + + match status: + + case 400: + + return "Bad request" + + case 401: + + return "Unauthorized" + + case 403: + + return "Forbidden" + + case 404: + + return "Not found" +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,13 +1,2 @@ + def http_status(status): +- match status: +- case 400: +- return "Bad request" +- +- case 401: +- return "Unauthorized" +- +- case 403: +- return "Forbidden" +- +- case 404: +- return "Not found" ++ NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Ruff Output + +```py +def http_status(status): + NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Black Output + +```py +def http_status(status): + match status: + case 400: + return "Bad request" + + case 401: + return "Unauthorized" + + case 403: + return "Forbidden" + + case 404: + return "Not found" +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__starred_for_target.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__starred_for_target.py.snap new file mode 100644 index 0000000000..da4a8b4b11 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__starred_for_target.py.snap @@ -0,0 +1,136 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py +--- +## Input + +```py +for x in *a, *b: + print(x) + +for x in a, b, *c: + print(x) + +for x in *a, b, c: + print(x) + +for x in *a, b, *c: + print(x) + +async for x in *a, *b: + print(x) + +async for x in *a, b, *c: + print(x) + +async for x in a, b, *c: + print(x) + +async for x in ( + *loooooooooooooooooooooong, + very, + *loooooooooooooooooooooooooooooooooooooooooooooooong, +): + print(x) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,27 +1,19 @@ +-for x in *a, *b: ++for x in *NOT_YET_IMPLEMENTED_ExprStarred, *NOT_YET_IMPLEMENTED_ExprStarred: + print(x) + +-for x in a, b, *c: ++for x in a, b, *NOT_YET_IMPLEMENTED_ExprStarred: + print(x) + +-for x in *a, b, c: ++for x in *NOT_YET_IMPLEMENTED_ExprStarred, b, c: + print(x) + +-for x in *a, b, *c: ++for x in *NOT_YET_IMPLEMENTED_ExprStarred, b, *NOT_YET_IMPLEMENTED_ExprStarred: + print(x) + +-async for x in *a, *b: +- print(x) ++NOT_YET_IMPLEMENTED_StmtAsyncFor + +-async for x in *a, b, *c: +- print(x) ++NOT_YET_IMPLEMENTED_StmtAsyncFor + +-async for x in a, b, *c: +- print(x) ++NOT_YET_IMPLEMENTED_StmtAsyncFor + +-async for x in ( +- *loooooooooooooooooooooong, +- very, +- *loooooooooooooooooooooooooooooooooooooooooooooooong, +-): +- print(x) ++NOT_YET_IMPLEMENTED_StmtAsyncFor +``` + +## Ruff Output + +```py +for x in *NOT_YET_IMPLEMENTED_ExprStarred, *NOT_YET_IMPLEMENTED_ExprStarred: + print(x) + +for x in a, b, *NOT_YET_IMPLEMENTED_ExprStarred: + print(x) + +for x in *NOT_YET_IMPLEMENTED_ExprStarred, b, c: + print(x) + +for x in *NOT_YET_IMPLEMENTED_ExprStarred, b, *NOT_YET_IMPLEMENTED_ExprStarred: + print(x) + +NOT_YET_IMPLEMENTED_StmtAsyncFor + +NOT_YET_IMPLEMENTED_StmtAsyncFor + +NOT_YET_IMPLEMENTED_StmtAsyncFor + +NOT_YET_IMPLEMENTED_StmtAsyncFor +``` + +## Black Output + +```py +for x in *a, *b: + print(x) + +for x in a, b, *c: + print(x) + +for x in *a, b, c: + print(x) + +for x in *a, b, *c: + print(x) + +async for x in *a, *b: + print(x) + +async for x in *a, b, *c: + print(x) + +async for x in a, b, *c: + print(x) + +async for x in ( + *loooooooooooooooooooooong, + very, + *loooooooooooooooooooooooooooooooooooooooooooooooong, +): + print(x) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654.py.snap new file mode 100644 index 0000000000..b7995833b5 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654.py.snap @@ -0,0 +1,240 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py +--- +## Input + +```py +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,5 +1,5 @@ + try: +- raise OSError("blah") ++ NOT_YET_IMPLEMENTED_StmtRaise + except* ExceptionGroup as e: + pass + +@@ -14,10 +14,10 @@ + + try: + try: +- raise ValueError(42) ++ NOT_YET_IMPLEMENTED_StmtRaise + except: + try: +- raise TypeError(int) ++ NOT_YET_IMPLEMENTED_StmtRaise + except* Exception: + pass + 1 / 0 +@@ -26,10 +26,10 @@ + + try: + try: +- raise FalsyEG("eg", [TypeError(1), ValueError(2)]) ++ NOT_YET_IMPLEMENTED_StmtRaise + except* TypeError as e: + tes = e +- raise ++ NOT_YET_IMPLEMENTED_StmtRaise + except* ValueError as e: + ves = e + pass +@@ -38,16 +38,16 @@ + + try: + try: +- raise orig ++ NOT_YET_IMPLEMENTED_StmtRaise + except* (TypeError, ValueError) as e: +- raise SyntaxError(3) from e ++ NOT_YET_IMPLEMENTED_StmtRaise + except BaseException as e: + exc = e + + try: + try: +- raise orig ++ NOT_YET_IMPLEMENTED_StmtRaise + except* OSError as e: +- raise TypeError(3) from e ++ NOT_YET_IMPLEMENTED_StmtRaise + except ExceptionGroup as e: + exc = e +``` + +## Ruff Output + +```py +try: + NOT_YET_IMPLEMENTED_StmtRaise +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* TypeError as e: + tes = e + NOT_YET_IMPLEMENTED_StmtRaise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* (TypeError, ValueError) as e: + NOT_YET_IMPLEMENTED_StmtRaise +except BaseException as e: + exc = e + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* OSError as e: + NOT_YET_IMPLEMENTED_StmtRaise +except ExceptionGroup as e: + exc = e +``` + +## Black Output + +```py +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654_style.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654_style.py.snap new file mode 100644 index 0000000000..19568a1176 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_311__pep_654_style.py.snap @@ -0,0 +1,243 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py +--- +## Input + +```py +try: + raise OSError("blah") +except * ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except *ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except *(Exception): + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except \ + *TypeError as e: + tes = e + raise + except * ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except *(TypeError, ValueError, *OTHER_EXCEPTIONS) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except\ + * OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,5 +1,5 @@ + try: +- raise OSError("blah") ++ NOT_YET_IMPLEMENTED_StmtRaise + except* ExceptionGroup as e: + pass + +@@ -14,10 +14,10 @@ + + try: + try: +- raise ValueError(42) ++ NOT_YET_IMPLEMENTED_StmtRaise + except: + try: +- raise TypeError(int) ++ NOT_YET_IMPLEMENTED_StmtRaise + except* Exception: + pass + 1 / 0 +@@ -26,10 +26,10 @@ + + try: + try: +- raise FalsyEG("eg", [TypeError(1), ValueError(2)]) ++ NOT_YET_IMPLEMENTED_StmtRaise + except* TypeError as e: + tes = e +- raise ++ NOT_YET_IMPLEMENTED_StmtRaise + except* ValueError as e: + ves = e + pass +@@ -38,16 +38,16 @@ + + try: + try: +- raise orig +- except* (TypeError, ValueError, *OTHER_EXCEPTIONS) as e: +- raise SyntaxError(3) from e ++ NOT_YET_IMPLEMENTED_StmtRaise ++ except* (TypeError, ValueError, *NOT_YET_IMPLEMENTED_ExprStarred) as e: ++ NOT_YET_IMPLEMENTED_StmtRaise + except BaseException as e: + exc = e + + try: + try: +- raise orig ++ NOT_YET_IMPLEMENTED_StmtRaise + except* OSError as e: +- raise TypeError(3) from e ++ NOT_YET_IMPLEMENTED_StmtRaise + except ExceptionGroup as e: + exc = e +``` + +## Ruff Output + +```py +try: + NOT_YET_IMPLEMENTED_StmtRaise +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* TypeError as e: + tes = e + NOT_YET_IMPLEMENTED_StmtRaise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* (TypeError, ValueError, *NOT_YET_IMPLEMENTED_ExprStarred) as e: + NOT_YET_IMPLEMENTED_StmtRaise +except BaseException as e: + exc = e + +try: + try: + NOT_YET_IMPLEMENTED_StmtRaise + except* OSError as e: + NOT_YET_IMPLEMENTED_StmtRaise +except ExceptionGroup as e: + exc = e +``` + +## Black Output + +```py +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError, *OTHER_EXCEPTIONS) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals.py.snap new file mode 100644 index 0000000000..4135458654 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals.py.snap @@ -0,0 +1,118 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py +--- +## Input + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 123456 +x = .1 +x = 1. +x = 1E+1 +x = 1E-1 +x = 1.000_000_01 +x = 123456789.123456789 +x = 123456789.123456789E123456789 +x = 123456789E123456789 +x = 123456789J +x = 123456789.123456789J +x = 0XB1ACC +x = 0B1011 +x = 0O777 +x = 0.000000006 +x = 10000 +x = 133333 +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -2,19 +2,19 @@ + + x = 123456789 + x = 123456 +-x = 0.1 +-x = 1.0 +-x = 1e1 +-x = 1e-1 ++x = .1 ++x = 1. ++x = 1E+1 ++x = 1E-1 + x = 1.000_000_01 + x = 123456789.123456789 +-x = 123456789.123456789e123456789 +-x = 123456789e123456789 +-x = 123456789j +-x = 123456789.123456789j +-x = 0xB1ACC +-x = 0b1011 +-x = 0o777 ++x = 123456789.123456789E123456789 ++x = 123456789E123456789 ++x = 123456789J ++x = 123456789.123456789J ++x = 0XB1ACC ++x = 0B1011 ++x = 0O777 + x = 0.000000006 + x = 10000 + x = 133333 +``` + +## Ruff Output + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 123456 +x = .1 +x = 1. +x = 1E+1 +x = 1E-1 +x = 1.000_000_01 +x = 123456789.123456789 +x = 123456789.123456789E123456789 +x = 123456789E123456789 +x = 123456789J +x = 123456789.123456789J +x = 0XB1ACC +x = 0B1011 +x = 0O777 +x = 0.000000006 +x = 10000 +x = 133333 +``` + +## Black Output + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 123456 +x = 0.1 +x = 1.0 +x = 1e1 +x = 1e-1 +x = 1.000_000_01 +x = 123456789.123456789 +x = 123456789.123456789e123456789 +x = 123456789e123456789 +x = 123456789j +x = 123456789.123456789j +x = 0xB1ACC +x = 0b1011 +x = 0o777 +x = 0.000000006 +x = 10000 +x = 133333 +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals_skip_underscores.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals_skip_underscores.py.snap new file mode 100644 index 0000000000..69d74787f3 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals_skip_underscores.py.snap @@ -0,0 +1,72 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py +--- +## Input + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 1_2_3_4_5_6_7 +x = 1E+1 +x = 0xb1acc +x = 0.00_00_006 +x = 12_34_567J +x = .1_2 +x = 1_2. +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -2,9 +2,9 @@ + + x = 123456789 + x = 1_2_3_4_5_6_7 +-x = 1e1 +-x = 0xB1ACC ++x = 1E+1 ++x = 0xb1acc + x = 0.00_00_006 +-x = 12_34_567j +-x = 0.1_2 +-x = 1_2.0 ++x = 12_34_567J ++x = .1_2 ++x = 1_2. +``` + +## Ruff Output + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 1_2_3_4_5_6_7 +x = 1E+1 +x = 0xb1acc +x = 0.00_00_006 +x = 12_34_567J +x = .1_2 +x = 1_2. +``` + +## Black Output + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 1_2_3_4_5_6_7 +x = 1e1 +x = 0xB1ACC +x = 0.00_00_006 +x = 12_34_567j +x = 0.1_2 +x = 1_2.0 +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap new file mode 100644 index 0000000000..47302f6806 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap @@ -0,0 +1,144 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py +--- +## Input + +```py +#!/usr/bin/env python3.7 + + +def f(): + return (i * 2 async for i in arange(42)) + + +def g(): + return ( + something_long * something_long + async for something_long in async_generator(with_an_argument) + ) + + +async def func(): + if test: + out_batched = [ + i + async for i in aitertools._async_map( + self.async_inc, arange(8), batch_size=3 + ) + ] + + +def awaited_generator_value(n): + return (await awaitable for awaitable in awaitable_list) + + +def make_arange(n): + return (i * 2 for i in range(n) if await wrap(i)) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -2,29 +2,21 @@ + + + def f(): +- return (i * 2 async for i in arange(42)) ++ return (i for i in []) + + + def g(): +- return ( +- something_long * something_long +- async for something_long in async_generator(with_an_argument) +- ) ++ return (i for i in []) + + + async def func(): + if test: +- out_batched = [ +- i +- async for i in aitertools._async_map( +- self.async_inc, arange(8), batch_size=3 +- ) +- ] ++ out_batched = [i for i in []] + + + def awaited_generator_value(n): +- return (await awaitable for awaitable in awaitable_list) ++ return (i for i in []) + + + def make_arange(n): +- return (i * 2 for i in range(n) if await wrap(i)) ++ return (i for i in []) +``` + +## Ruff Output + +```py +#!/usr/bin/env python3.7 + + +def f(): + return (i for i in []) + + +def g(): + return (i for i in []) + + +async def func(): + if test: + out_batched = [i for i in []] + + +def awaited_generator_value(n): + return (i for i in []) + + +def make_arange(n): + return (i for i in []) +``` + +## Black Output + +```py +#!/usr/bin/env python3.7 + + +def f(): + return (i * 2 async for i in arange(42)) + + +def g(): + return ( + something_long * something_long + async for something_long in async_generator(with_an_argument) + ) + + +async def func(): + if test: + out_batched = [ + i + async for i in aitertools._async_map( + self.async_inc, arange(8), batch_size=3 + ) + ] + + +def awaited_generator_value(n): + return (await awaitable for awaitable in awaitable_list) + + +def make_arange(n): + return (i * 2 for i in range(n) if await wrap(i)) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_570.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_570.py.snap new file mode 100644 index 0000000000..fb9745c292 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_570.py.snap @@ -0,0 +1,174 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py +--- +## Input + +```py +def positional_only_arg(a, /): + pass + + +def all_markers(a, b, /, c, d, *, e, f): + pass + + +def all_markers_with_args_and_kwargs( + a_long_one, + b_long_one, + /, + c_long_one, + d_long_one, + *args, + e_long_one, + f_long_one, + **kwargs, +): + pass + + +def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5): + pass + + +def long_one_with_long_parameter_names( + but_all_of_them, + are_positional_only, + arguments_mmmmkay, + so_this_is_only_valid_after, + three_point_eight, + /, +): + pass + + +lambda a, /: a + +lambda a, b, /, c, d, *, e, f: a + +lambda a, b, /, c, d, *args, e, f, **kwargs: args + +lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -35,10 +35,10 @@ + pass + + +-lambda a, /: a ++lambda x: True + +-lambda a, b, /, c, d, *, e, f: a ++lambda x: True + +-lambda a, b, /, c, d, *args, e, f, **kwargs: args ++lambda x: True + +-lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 ++lambda x: True +``` + +## Ruff Output + +```py +def positional_only_arg(a, /): + pass + + +def all_markers(a, b, /, c, d, *, e, f): + pass + + +def all_markers_with_args_and_kwargs( + a_long_one, + b_long_one, + /, + c_long_one, + d_long_one, + *args, + e_long_one, + f_long_one, + **kwargs, +): + pass + + +def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5): + pass + + +def long_one_with_long_parameter_names( + but_all_of_them, + are_positional_only, + arguments_mmmmkay, + so_this_is_only_valid_after, + three_point_eight, + /, +): + pass + + +lambda x: True + +lambda x: True + +lambda x: True + +lambda x: True +``` + +## Black Output + +```py +def positional_only_arg(a, /): + pass + + +def all_markers(a, b, /, c, d, *, e, f): + pass + + +def all_markers_with_args_and_kwargs( + a_long_one, + b_long_one, + /, + c_long_one, + d_long_one, + *args, + e_long_one, + f_long_one, + **kwargs, +): + pass + + +def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5): + pass + + +def long_one_with_long_parameter_names( + but_all_of_them, + are_positional_only, + arguments_mmmmkay, + so_this_is_only_valid_after, + three_point_eight, + /, +): + pass + + +lambda a, /: a + +lambda a, b, /, c, d, *, e, f: a + +lambda a, b, /, c, d, *args, e, f, **kwargs: args + +lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap new file mode 100644 index 0000000000..5e1fa92d6b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap @@ -0,0 +1,247 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py +--- +## Input + +```py +(a := 1) +(a := a) +if (match := pattern.search(data)) is None: + pass +if match := pattern.search(data): + pass +[y := f(x), y**2, y**3] +filtered_data = [y for x in data if (y := f(x)) is None] +(y := f(x)) +y0 = (y1 := f(x)) +foo(x=(y := f(x))) + + +def foo(answer=(p := 42)): + pass + + +def foo(answer: (p := 42) = 5): + pass + + +lambda: (x := 1) +(x := lambda: 1) +(x := lambda: (y := 1)) +lambda line: (m := re.match(pattern, line)) and m.group(1) +x = (y := 0) +(z := (y := (x := 0))) +(info := (name, phone, *rest)) +(x := 1, 2) +(total := total + tax) +len(lines := f.readlines()) +foo(x := 3, cat="vector") +foo(cat=(category := "vector")) +if any(len(longline := l) >= 100 for l in lines): + print(longline) +if env_base := os.environ.get("PYTHONUSERBASE", None): + return env_base +if self._is_special and (ans := self._check_nans(context=context)): + return ans +foo(b := 2, a=1) +foo((b := 2), a=1) +foo(c=(b := 2), a=1) + +while x := f(x): + pass +while x := f(x): + pass +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,47 +1,47 @@ +-(a := 1) +-(a := a) +-if (match := pattern.search(data)) is None: ++(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++if (NOT_YET_IMPLEMENTED_ExprNamedExpr) is None: + pass +-if match := pattern.search(data): ++if NOT_YET_IMPLEMENTED_ExprNamedExpr: + pass +-[y := f(x), y**2, y**3] +-filtered_data = [y for x in data if (y := f(x)) is None] +-(y := f(x)) +-y0 = (y1 := f(x)) +-foo(x=(y := f(x))) ++[NOT_YET_IMPLEMENTED_ExprNamedExpr, y**2, y**3] ++filtered_data = [i for i in []] ++(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++y0 = NOT_YET_IMPLEMENTED_ExprNamedExpr ++foo(x=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) + + +-def foo(answer=(p := 42)): ++def foo(answer=(NOT_YET_IMPLEMENTED_ExprNamedExpr)): + pass + + +-def foo(answer: (p := 42) = 5): ++def foo(answer: (NOT_YET_IMPLEMENTED_ExprNamedExpr) = 5): + pass + + +-lambda: (x := 1) +-(x := lambda: 1) +-(x := lambda: (y := 1)) +-lambda line: (m := re.match(pattern, line)) and m.group(1) +-x = (y := 0) +-(z := (y := (x := 0))) +-(info := (name, phone, *rest)) +-(x := 1, 2) +-(total := total + tax) +-len(lines := f.readlines()) +-foo(x := 3, cat="vector") +-foo(cat=(category := "vector")) +-if any(len(longline := l) >= 100 for l in lines): ++lambda x: True ++(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++lambda x: True ++x = NOT_YET_IMPLEMENTED_ExprNamedExpr ++(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++(NOT_YET_IMPLEMENTED_ExprNamedExpr, 2) ++(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++len(NOT_YET_IMPLEMENTED_ExprNamedExpr) ++foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, cat="vector") ++foo(cat=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) ++if any((i for i in [])): + print(longline) +-if env_base := os.environ.get("PYTHONUSERBASE", None): ++if NOT_YET_IMPLEMENTED_ExprNamedExpr: + return env_base +-if self._is_special and (ans := self._check_nans(context=context)): ++if self._is_special and (NOT_YET_IMPLEMENTED_ExprNamedExpr): + return ans +-foo(b := 2, a=1) +-foo((b := 2), a=1) +-foo(c=(b := 2), a=1) ++foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1) ++foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1) ++foo(c=(NOT_YET_IMPLEMENTED_ExprNamedExpr), a=1) + +-while x := f(x): ++while NOT_YET_IMPLEMENTED_ExprNamedExpr: + pass +-while x := f(x): ++while NOT_YET_IMPLEMENTED_ExprNamedExpr: + pass +``` + +## Ruff Output + +```py +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +if (NOT_YET_IMPLEMENTED_ExprNamedExpr) is None: + pass +if NOT_YET_IMPLEMENTED_ExprNamedExpr: + pass +[NOT_YET_IMPLEMENTED_ExprNamedExpr, y**2, y**3] +filtered_data = [i for i in []] +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +y0 = NOT_YET_IMPLEMENTED_ExprNamedExpr +foo(x=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) + + +def foo(answer=(NOT_YET_IMPLEMENTED_ExprNamedExpr)): + pass + + +def foo(answer: (NOT_YET_IMPLEMENTED_ExprNamedExpr) = 5): + pass + + +lambda x: True +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +lambda x: True +x = NOT_YET_IMPLEMENTED_ExprNamedExpr +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +(NOT_YET_IMPLEMENTED_ExprNamedExpr, 2) +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +len(NOT_YET_IMPLEMENTED_ExprNamedExpr) +foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, cat="vector") +foo(cat=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) +if any((i for i in [])): + print(longline) +if NOT_YET_IMPLEMENTED_ExprNamedExpr: + return env_base +if self._is_special and (NOT_YET_IMPLEMENTED_ExprNamedExpr): + return ans +foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1) +foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, a=1) +foo(c=(NOT_YET_IMPLEMENTED_ExprNamedExpr), a=1) + +while NOT_YET_IMPLEMENTED_ExprNamedExpr: + pass +while NOT_YET_IMPLEMENTED_ExprNamedExpr: + pass +``` + +## Black Output + +```py +(a := 1) +(a := a) +if (match := pattern.search(data)) is None: + pass +if match := pattern.search(data): + pass +[y := f(x), y**2, y**3] +filtered_data = [y for x in data if (y := f(x)) is None] +(y := f(x)) +y0 = (y1 := f(x)) +foo(x=(y := f(x))) + + +def foo(answer=(p := 42)): + pass + + +def foo(answer: (p := 42) = 5): + pass + + +lambda: (x := 1) +(x := lambda: 1) +(x := lambda: (y := 1)) +lambda line: (m := re.match(pattern, line)) and m.group(1) +x = (y := 0) +(z := (y := (x := 0))) +(info := (name, phone, *rest)) +(x := 1, 2) +(total := total + tax) +len(lines := f.readlines()) +foo(x := 3, cat="vector") +foo(cat=(category := "vector")) +if any(len(longline := l) >= 100 for l in lines): + print(longline) +if env_base := os.environ.get("PYTHONUSERBASE", None): + return env_base +if self._is_special and (ans := self._check_nans(context=context)): + return ans +foo(b := 2, a=1) +foo((b := 2), a=1) +foo(c=(b := 2), a=1) + +while x := f(x): + pass +while x := f(x): + pass +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap new file mode 100644 index 0000000000..4f4ceeb54b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap @@ -0,0 +1,113 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py +--- +## Input + +```py +#!/usr/bin/env python3.8 + + +def starred_return(): + my_list = ["value2", "value3"] + return "value1", *my_list + + +def starred_yield(): + my_list = ["value2", "value3"] + yield "value1", *my_list + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +a : Tuple[ str, int] = "1", 2 +a: Tuple[int , ... ] = b, *c, d +def t(): + a : str = yield "a" +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -3,19 +3,19 @@ + + def starred_return(): + my_list = ["value2", "value3"] +- return "value1", *my_list ++ return "value1", *NOT_YET_IMPLEMENTED_ExprStarred + + + def starred_yield(): + my_list = ["value2", "value3"] +- yield "value1", *my_list ++ NOT_YET_IMPLEMENTED_ExprYield + + + # all right hand side expressions allowed in regular assignments are now also allowed in + # annotated assignments +-a: Tuple[str, int] = "1", 2 +-a: Tuple[int, ...] = b, *c, d ++NOT_YET_IMPLEMENTED_StmtAnnAssign ++NOT_YET_IMPLEMENTED_StmtAnnAssign + + + def t(): +- a: str = yield "a" ++ NOT_YET_IMPLEMENTED_StmtAnnAssign +``` + +## Ruff Output + +```py +#!/usr/bin/env python3.8 + + +def starred_return(): + my_list = ["value2", "value3"] + return "value1", *NOT_YET_IMPLEMENTED_ExprStarred + + +def starred_yield(): + my_list = ["value2", "value3"] + NOT_YET_IMPLEMENTED_ExprYield + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +NOT_YET_IMPLEMENTED_StmtAnnAssign +NOT_YET_IMPLEMENTED_StmtAnnAssign + + +def t(): + NOT_YET_IMPLEMENTED_StmtAnnAssign +``` + +## Black Output + +```py +#!/usr/bin/env python3.8 + + +def starred_return(): + my_list = ["value2", "value3"] + return "value1", *my_list + + +def starred_yield(): + my_list = ["value2", "value3"] + yield "value1", *my_list + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +a: Tuple[str, int] = "1", 2 +a: Tuple[int, ...] = b, *c, d + + +def t(): + a: str = yield "a" +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__pep_572_py39.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__pep_572_py39.py.snap new file mode 100644 index 0000000000..ec43406b54 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__pep_572_py39.py.snap @@ -0,0 +1,60 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py +--- +## Input + +```py +# Unparenthesized walruses are now allowed in set literals & set comprehensions +# since Python 3.9 +{x := 1, 2, 3} +{x4 := x**5 for x in range(7)} +# We better not remove the parentheses here (since it's a 3.10 feature) +x[(a := 1)] +x[(a := 1), (b := 3)] +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,7 +1,7 @@ + # Unparenthesized walruses are now allowed in set literals & set comprehensions + # since Python 3.9 +-{x := 1, 2, 3} +-{x4 := x**5 for x in range(7)} ++{NOT_YET_IMPLEMENTED_ExprNamedExpr, 2, 3} ++{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} + # We better not remove the parentheses here (since it's a 3.10 feature) +-x[(a := 1)] +-x[(a := 1), (b := 3)] ++x[(NOT_YET_IMPLEMENTED_ExprNamedExpr)] ++x[((NOT_YET_IMPLEMENTED_ExprNamedExpr), (NOT_YET_IMPLEMENTED_ExprNamedExpr))] +``` + +## Ruff Output + +```py +# Unparenthesized walruses are now allowed in set literals & set comprehensions +# since Python 3.9 +{NOT_YET_IMPLEMENTED_ExprNamedExpr, 2, 3} +{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} +# We better not remove the parentheses here (since it's a 3.10 feature) +x[(NOT_YET_IMPLEMENTED_ExprNamedExpr)] +x[((NOT_YET_IMPLEMENTED_ExprNamedExpr), (NOT_YET_IMPLEMENTED_ExprNamedExpr))] +``` + +## Black Output + +```py +# Unparenthesized walruses are now allowed in set literals & set comprehensions +# since Python 3.9 +{x := 1, 2, 3} +{x4 := x**5 for x in range(7)} +# We better not remove the parentheses here (since it's a 3.10 feature) +x[(a := 1)] +x[(a := 1), (b := 3)] +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap new file mode 100644 index 0000000000..86ad5e3c92 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap @@ -0,0 +1,94 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py +--- +## Input + +```py +#!/usr/bin/env python3.9 + +@relaxed_decorator[0] +def f(): + ... + +@relaxed_decorator[extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length] +def f(): + ... + +@extremely_long_variable_name_that_doesnt_fit := complex.expression(with_long="arguments_value_that_wont_fit_at_the_end_of_the_line") +def f(): + ... +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,6 +1,5 @@ + #!/usr/bin/env python3.9 + +- + @relaxed_decorator[0] + def f(): + ... +@@ -13,8 +12,6 @@ + ... + + +-@extremely_long_variable_name_that_doesnt_fit := complex.expression( +- with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" +-) ++@NOT_YET_IMPLEMENTED_ExprNamedExpr + def f(): + ... +``` + +## Ruff Output + +```py +#!/usr/bin/env python3.9 + +@relaxed_decorator[0] +def f(): + ... + + +@relaxed_decorator[ + extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length +] +def f(): + ... + + +@NOT_YET_IMPLEMENTED_ExprNamedExpr +def f(): + ... +``` + +## Black Output + +```py +#!/usr/bin/env python3.9 + + +@relaxed_decorator[0] +def f(): + ... + + +@relaxed_decorator[ + extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length +] +def f(): + ... + + +@extremely_long_variable_name_that_doesnt_fit := complex.expression( + with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" +) +def f(): + ... +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__remove_with_brackets.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__remove_with_brackets.py.snap new file mode 100644 index 0000000000..8766515712 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__remove_with_brackets.py.snap @@ -0,0 +1,216 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py +--- +## Input + +```py +with (open("bla.txt")): + pass + +with (open("bla.txt")), (open("bla.txt")): + pass + +with (open("bla.txt") as f): + pass + +# Remove brackets within alias expression +with (open("bla.txt")) as f: + pass + +# Remove brackets around one-line context managers +with (open("bla.txt") as f, (open("x"))): + pass + +with ((open("bla.txt")) as f, open("x")): + pass + +with (CtxManager1() as example1, CtxManager2() as example2): + ... + +# Brackets remain when using magic comma +with (CtxManager1() as example1, CtxManager2() as example2,): + ... + +# Brackets remain for multi-line context managers +with (CtxManager1() as example1, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2): + ... + +# Don't touch assignment expressions +with (y := open("./test.py")) as f: + pass + +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with (((open("bla.txt")))): + pass + +with (((open("bla.txt")))), (((open("bla.txt")))): + pass + +with (((open("bla.txt")))) as f: + pass + +with ((((open("bla.txt")))) as f): + pass + +with ((((CtxManager1()))) as example1, (((CtxManager2()))) as example2): + ... +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -39,7 +39,7 @@ + ... + + # Don't touch assignment expressions +-with (y := open("./test.py")) as f: ++with NOT_YET_IMPLEMENTED_ExprNamedExpr as f: + pass + + # Deeply nested examples +``` + +## Ruff Output + +```py +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +# Remove brackets within alias expression +with open("bla.txt") as f: + pass + +# Remove brackets around one-line context managers +with open("bla.txt") as f, open("x"): + pass + +with open("bla.txt") as f, open("x"): + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... + +# Brackets remain when using magic comma +with ( + CtxManager1() as example1, + CtxManager2() as example2, +): + ... + +# Brackets remain for multi-line context managers +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, +): + ... + +# Don't touch assignment expressions +with NOT_YET_IMPLEMENTED_ExprNamedExpr as f: + pass + +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +with open("bla.txt") as f: + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... +``` + +## Black Output + +```py +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +# Remove brackets within alias expression +with open("bla.txt") as f: + pass + +# Remove brackets around one-line context managers +with open("bla.txt") as f, open("x"): + pass + +with open("bla.txt") as f, open("x"): + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... + +# Brackets remain when using magic comma +with ( + CtxManager1() as example1, + CtxManager2() as example2, +): + ... + +# Brackets remain for multi-line context managers +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, +): + ... + +# Don't touch assignment expressions +with (y := open("./test.py")) as f: + pass + +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +with open("bla.txt") as f: + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@attribute_access_on_number_literals.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__attribute_access_on_number_literals.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@attribute_access_on_number_literals.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__attribute_access_on_number_literals.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@bracketmatch.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__bracketmatch.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@bracketmatch.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__bracketmatch.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@class_methods_new_line.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__class_methods_new_line.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@class_methods_new_line.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__class_methods_new_line.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@collections.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__collections.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@collections.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__collections.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comment_after_escaped_newline.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comment_after_escaped_newline.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comment_after_escaped_newline.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comment_after_escaped_newline.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments.py.snap similarity index 97% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments.py.snap index f5c40ca6bc..d78df8d356 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments.py.snap @@ -140,13 +140,6 @@ async def wat(): if inner_imports.are_evil(): # Explains why we have this if. -@@ -93,4 +93,4 @@ - - # Some closing comments. - # Maybe Vim or Emacs directives for formatting. --# Who knows. -\ No newline at end of file -+# Who knows. ``` ## Ruff Output @@ -348,6 +341,7 @@ async def wat(): # Some closing comments. # Maybe Vim or Emacs directives for formatting. -# Who knows.``` +# Who knows. +``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments2.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments3.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments4.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments4.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments4.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments4.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments5.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments5.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments5.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments6.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments6.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments6.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments6.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments9.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments9.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments9.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments9.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments_non_breaking_space.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@comments_non_breaking_space.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition_no_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition_no_trailing_comma.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@composition_no_trailing_comma.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition_no_trailing_comma.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@docstring.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@docstring_preview.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring_preview.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@docstring_preview.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring_preview.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@empty_lines.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__empty_lines.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@empty_lines.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__empty_lines.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap similarity index 99% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap index a2b53747c5..9d7a94ba9a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@expression.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap @@ -266,15 +266,15 @@ last_call() ```diff --- Black +++ Ruff -@@ -1,5 +1,6 @@ -+... +@@ -1,6 +1,6 @@ + ... "some_string" -b"\\xa3" +b"NOT_YET_IMPLEMENTED_BYTE_STRING" Name None True -@@ -23,40 +24,46 @@ +@@ -24,40 +24,46 @@ 1 >> v2 1 % finished 1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 @@ -346,7 +346,7 @@ last_call() () (1,) (1, 2) -@@ -68,40 +75,37 @@ +@@ -69,40 +75,37 @@ 2, 3, ] @@ -405,9 +405,12 @@ last_call() Python3 > Python2 > COBOL Life is Life call() -@@ -116,8 +120,8 @@ +@@ -115,10 +118,10 @@ + arg, + another, kwarg="hey", - **kwargs, +- **kwargs ++ **kwargs, ) # note: no trailing comma pre-3.6 -call(*gidgets[:2]) -call(a, *gidgets[:2]) @@ -416,7 +419,7 @@ last_call() call(**self.screen_kwargs) call(b, **self.screen_kwargs) lukasz.langa.pl -@@ -130,34 +134,28 @@ +@@ -131,34 +134,28 @@ tuple[str, ...] tuple[str, int, float, dict[str, int]] tuple[ @@ -424,9 +427,6 @@ last_call() - int, - float, - dict[str, int], --] --very_long_variable_name_filters: t.List[ -- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], + ( + str, + int, @@ -434,6 +434,9 @@ last_call() + dict[str, int], + ) ] +-very_long_variable_name_filters: t.List[ +- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], +-] -xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore - sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) -) @@ -464,7 +467,7 @@ last_call() numpy[0, :] numpy[:, i] numpy[0, :2] -@@ -171,20 +169,27 @@ +@@ -172,20 +169,27 @@ numpy[1 : c + 1, c] numpy[-(c + 1) :, d] numpy[:, l[-2]] @@ -500,7 +503,7 @@ last_call() { "id": "1", "type": "type", -@@ -199,32 +204,22 @@ +@@ -200,32 +204,22 @@ c = 1 d = (1,) + a + (2,) e = (1,).count(1) @@ -513,7 +516,7 @@ last_call() ) what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( vars_to_remove - ) +-) -result = ( - session.query(models.Customer.id) - .filter( @@ -521,7 +524,7 @@ last_call() - ) - .order_by(models.Customer.id.asc()) - .all() --) + ) -result = ( - session.query(models.Customer.id) - .filter( @@ -543,7 +546,7 @@ last_call() Ø = set() authors.łukasz.say_thanks() mapping = { -@@ -236,29 +231,27 @@ +@@ -237,29 +231,27 @@ def gen(): @@ -584,7 +587,7 @@ last_call() ... for i in call(): ... -@@ -327,13 +320,18 @@ +@@ -328,13 +320,18 @@ ): return True if ( @@ -606,7 +609,7 @@ last_call() ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True -@@ -341,7 +339,8 @@ +@@ -342,7 +339,8 @@ ~aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e @@ -995,6 +998,7 @@ last_call() ## Black Output ```py +... "some_string" b"\\xa3" Name @@ -1111,7 +1115,7 @@ call( arg, another, kwarg="hey", - **kwargs, + **kwargs ) # note: no trailing comma pre-3.6 call(*gidgets[:2]) call(a, *gidgets[:2]) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap similarity index 99% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap index e4f11e148d..4d40d39ee7 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap @@ -198,18 +198,17 @@ d={'a':1, ```diff --- Black +++ Ruff -@@ -1,16 +1,14 @@ +@@ -1,15 +1,14 @@ #!/usr/bin/env python3 -import asyncio -import sys -- --from third_party import X, Y, Z +NOT_YET_IMPLEMENTED_StmtImport +NOT_YET_IMPLEMENTED_StmtImport --from library import some_connection, some_decorator +-from third_party import X, Y, Z +NOT_YET_IMPLEMENTED_StmtImportFrom +-from library import some_connection, some_decorator +NOT_YET_IMPLEMENTED_StmtImportFrom # fmt: off -from third_party import (X, @@ -221,7 +220,7 @@ d={'a':1, # Comment 1 # Comment 2 -@@ -18,30 +16,54 @@ +@@ -17,30 +16,54 @@ # fmt: off def func_no_args(): @@ -297,7 +296,7 @@ d={'a':1, def spaces_types( -@@ -51,7 +73,7 @@ +@@ -50,7 +73,7 @@ d: dict = {}, e: bool = True, f: int = -1, @@ -306,7 +305,7 @@ d={'a':1, h: str = "", i: str = r"", ): -@@ -64,55 +86,54 @@ +@@ -63,55 +86,54 @@ something = { # fmt: off @@ -381,7 +380,7 @@ d={'a':1, # fmt: on -@@ -133,10 +154,10 @@ +@@ -132,10 +154,10 @@ """Another known limitation.""" # fmt: on # fmt: off @@ -396,7 +395,7 @@ d={'a':1, # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -151,12 +172,10 @@ +@@ -150,12 +172,10 @@ ast_args.kw_defaults, parameters, implicit_default=True, @@ -411,7 +410,7 @@ d={'a':1, # fmt: on _type_comment_re = re.compile( r""" -@@ -179,7 +198,7 @@ +@@ -178,7 +198,7 @@ $ """, # fmt: off @@ -420,7 +419,7 @@ d={'a':1, # fmt: on ) -@@ -217,8 +236,7 @@ +@@ -216,8 +236,7 @@ xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, ) # fmt: off @@ -691,7 +690,6 @@ import sys from third_party import X, Y, Z from library import some_connection, some_decorator - # fmt: off from third_party import (X, Y, Z) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff2.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff3.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff3.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff3.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff4.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff4.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtonoff5.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtpass_imports.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtpass_imports.py.snap new file mode 100644 index 0000000000..5a0bfd673f --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtpass_imports.py.snap @@ -0,0 +1,114 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py +--- +## Input + +```py +# Regression test for https://github.com/psf/black/issues/3438 + +import ast +import collections # fmt: skip +import dataclasses +# fmt: off +import os +# fmt: on +import pathlib + +import re # fmt: skip +import secrets + +# fmt: off +import sys +# fmt: on + +import tempfile +import zoneinfo +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,19 +1,19 @@ + # Regression test for https://github.com/psf/black/issues/3438 + +-import ast +-import collections # fmt: skip +-import dataclasses ++NOT_YET_IMPLEMENTED_StmtImport ++NOT_YET_IMPLEMENTED_StmtImport # fmt: skip ++NOT_YET_IMPLEMENTED_StmtImport + # fmt: off +-import os ++NOT_YET_IMPLEMENTED_StmtImport + # fmt: on +-import pathlib ++NOT_YET_IMPLEMENTED_StmtImport + +-import re # fmt: skip +-import secrets ++NOT_YET_IMPLEMENTED_StmtImport # fmt: skip ++NOT_YET_IMPLEMENTED_StmtImport + + # fmt: off +-import sys ++NOT_YET_IMPLEMENTED_StmtImport + # fmt: on + +-import tempfile +-import zoneinfo ++NOT_YET_IMPLEMENTED_StmtImport ++NOT_YET_IMPLEMENTED_StmtImport +``` + +## Ruff Output + +```py +# Regression test for https://github.com/psf/black/issues/3438 + +NOT_YET_IMPLEMENTED_StmtImport +NOT_YET_IMPLEMENTED_StmtImport # fmt: skip +NOT_YET_IMPLEMENTED_StmtImport +# fmt: off +NOT_YET_IMPLEMENTED_StmtImport +# fmt: on +NOT_YET_IMPLEMENTED_StmtImport + +NOT_YET_IMPLEMENTED_StmtImport # fmt: skip +NOT_YET_IMPLEMENTED_StmtImport + +# fmt: off +NOT_YET_IMPLEMENTED_StmtImport +# fmt: on + +NOT_YET_IMPLEMENTED_StmtImport +NOT_YET_IMPLEMENTED_StmtImport +``` + +## Black Output + +```py +# Regression test for https://github.com/psf/black/issues/3438 + +import ast +import collections # fmt: skip +import dataclasses +# fmt: off +import os +# fmt: on +import pathlib + +import re # fmt: skip +import secrets + +# fmt: off +import sys +# fmt: on + +import tempfile +import zoneinfo +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip2.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip2.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip2.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip3.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip3.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip3.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip5.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip5.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip5.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip7.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip7.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip7.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip7.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip8.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fmtskip8.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip8.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap similarity index 85% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@fstring.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap index 852778fb08..aa8a125ac7 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@fstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap @@ -14,6 +14,8 @@ f"{f'''{'nested'} inner'''} outer" f"\"{f'{nested} inner'}\" outer" f"space between opening braces: { {a for a in (1, 2, 3)}}" f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" ``` ## Black Differences @@ -21,7 +23,7 @@ f'Hello \'{tricky + "example"}\'' ```diff --- Black +++ Ruff -@@ -1,9 +1,9 @@ +@@ -1,11 +1,10 @@ -f"f-string without formatted values is just a string" -f"{{NOT a formatted value}}" -f'{{NOT \'a\' "formatted" "value"}}' @@ -31,6 +33,9 @@ f'Hello \'{tricky + "example"}\'' -f"\"{f'{nested} inner'}\" outer" -f"space between opening braces: { {a for a in (1, 2, 3)}}" -f'Hello \'{tricky + "example"}\'' +-f"Tried directories {str(rootdirs)} \ +-but none started with prefix {parentdir_prefix}" ++NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr @@ -54,6 +59,7 @@ NOT_YET_IMPLEMENTED_ExprJoinedStr NOT_YET_IMPLEMENTED_ExprJoinedStr NOT_YET_IMPLEMENTED_ExprJoinedStr NOT_YET_IMPLEMENTED_ExprJoinedStr +NOT_YET_IMPLEMENTED_ExprJoinedStr ``` ## Black Output @@ -68,6 +74,8 @@ f"{f'''{'nested'} inner'''} outer" f"\"{f'{nested} inner'}\" outer" f"space between opening braces: { {a for a in (1, 2, 3)}}" f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap similarity index 97% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap index 960b474e66..eed13bbae7 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap @@ -41,7 +41,7 @@ def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r'' def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... def spaces2(result= _core.Value(None)): assert fut is self._read_fut, (fut, self._read_fut) - + # EMPTY LINE WITH WHITESPACE (this comment will be removed) def example(session): result = session.query(models.Customer.id).filter( models.Customer.account_id == account_id, @@ -111,14 +111,14 @@ def __await__(): return (yield) #!/usr/bin/env python3 -import asyncio -import sys -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport - +- -from third_party import X, Y, Z -+NOT_YET_IMPLEMENTED_StmtImportFrom ++NOT_YET_IMPLEMENTED_StmtImport ++NOT_YET_IMPLEMENTED_StmtImport -from library import some_connection, some_decorator -- ++NOT_YET_IMPLEMENTED_StmtImportFrom + -f"trigger 3.6 mode" +NOT_YET_IMPLEMENTED_StmtImportFrom +NOT_YET_IMPLEMENTED_ExprJoinedStr @@ -170,12 +170,13 @@ def __await__(): return (yield) h: str = "", i: str = r"", ): -@@ -64,19 +73,16 @@ +@@ -64,19 +73,17 @@ def spaces2(result=_core.Value(None)): - assert fut is self._read_fut, (fut, self._read_fut) + NOT_YET_IMPLEMENTED_StmtAssert ++ # EMPTY LINE WITH WHITESPACE (this comment will be removed) def example(session): @@ -197,7 +198,7 @@ def __await__(): return (yield) def long_lines(): -@@ -87,7 +93,7 @@ +@@ -87,7 +94,7 @@ ast_args.kw_defaults, parameters, implicit_default=True, @@ -206,7 +207,7 @@ def __await__(): return (yield) ) typedargslist.extend( gen_annotated_params( -@@ -96,7 +102,7 @@ +@@ -96,7 +103,7 @@ parameters, implicit_default=True, # trailing standalone comment @@ -215,7 +216,7 @@ def __await__(): return (yield) ) _type_comment_re = re.compile( r""" -@@ -135,14 +141,8 @@ +@@ -135,14 +142,8 @@ a, **kwargs, ) -> A: @@ -313,6 +314,7 @@ def spaces_types( def spaces2(result=_core.Value(None)): NOT_YET_IMPLEMENTED_StmtAssert + # EMPTY LINE WITH WHITESPACE (this comment will be removed) def example(session): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function2.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@function2.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function2.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@function_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function_trailing_comma.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@function_trailing_comma.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function_trailing_comma.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@import_spacing.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__import_spacing.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@import_spacing.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__import_spacing.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@one_element_subscript.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__one_element_subscript.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@one_element_subscript.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__one_element_subscript.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@power_op_spacing.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__power_op_spacing.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@power_op_spacing.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__power_op_spacing.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_await_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_await_parens.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_await_parens.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_await_parens.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_except_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_except_parens.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_except_parens.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_except_parens.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_for_brackets.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_for_brackets.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_for_brackets.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_for_brackets.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_newline_after_code_block_open.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_newline_after_code_block_open.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_newline_after_code_block_open.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_newline_after_code_block_open.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_parens.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@remove_parens.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_parens.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@return_annotation_brackets.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__return_annotation_brackets.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@return_annotation_brackets.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__return_annotation_brackets.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__skip_magic_trailing_comma.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@skip_magic_trailing_comma.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__skip_magic_trailing_comma.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap similarity index 80% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap index 50113ee3a3..bf646fcda1 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@slices.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap @@ -36,38 +36,6 @@ ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] ham[lower + offset : upper + offset] - -slice[::, ::] -slice[ - # A - : - # B - : - # C -] -slice[ - # A - 1: - # B - 2: - # C - 3 -] - -slice[ - # A - 1 - + 2 : - # B - 3 : - # C - 4 -] -x[ - 1: # A - 2: # B - 3 # C -] ``` ## Black Differences @@ -75,7 +43,7 @@ x[ ```diff --- Black +++ Ruff -@@ -4,30 +4,30 @@ +@@ -4,28 +4,28 @@ slice[d::d] slice[0] slice[-1] @@ -113,27 +81,6 @@ x[ -ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] +ham[ : upper_fn(x) : step_fn(x)], ham[ :: step_fn(x)] ham[lower + offset : upper + offset] - - slice[::, ::] -@@ -49,11 +49,14 @@ - - slice[ - # A -- 1 -- + 2 : -+ 1 + 2 : - # B -- 3 : -+ 3 : - # C - 4 - ] --x[1:2:3] # A # B # C -+x[ -+ 1: # A -+ 2: # B -+ 3 # C -+] ``` ## Ruff Output @@ -170,37 +117,6 @@ ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] ham[ : upper_fn(x) : step_fn(x)], ham[ :: step_fn(x)] ham[lower + offset : upper + offset] - -slice[::, ::] -slice[ - # A - : - # B - : - # C -] -slice[ - # A - 1: - # B - 2: - # C - 3 -] - -slice[ - # A - 1 + 2 : - # B - 3 : - # C - 4 -] -x[ - 1: # A - 2: # B - 3 # C -] ``` ## Black Output @@ -237,34 +153,6 @@ ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] ham[lower + offset : upper + offset] - -slice[::, ::] -slice[ - # A - : - # B - : - # C -] -slice[ - # A - 1: - # B - 2: - # C - 3 -] - -slice[ - # A - 1 - + 2 : - # B - 3 : - # C - 4 -] -x[1:2:3] # A # B # C ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@string_prefixes.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__string_prefixes.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@string_prefixes.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__string_prefixes.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@torture.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__torture.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@torture.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__torture.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens1.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens1.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens1.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens1.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens2.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_comma_optional_parens2.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens2.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_commas_in_leading_parts.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@trailing_commas_in_leading_parts.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@tupleassign.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__tupleassign.py.snap similarity index 100% rename from crates/ruff_python_formatter/tests/snapshots/black_compatibility@tupleassign.py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__tupleassign.py.snap diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__whitespace.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__whitespace.py.snap new file mode 100644 index 0000000000..70636c22b7 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__whitespace.py.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/whitespace.py +--- +## Input + +```py + +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1 +0,0 @@ +- +``` + +## Ruff Output + +```py +``` + +## Black Output + +```py + +``` + +