From 16be691712a3d58aa1b0f148e6bdf600fea6928d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 27 Feb 2023 18:21:53 -0500 Subject: [PATCH] Enable more non-panicking formatter tests (#3262) --- crates/ruff_python_formatter/src/lib.rs | 8 +- ...tter__tests__black_test__comments3_py.snap | 213 ++++ ...tter__tests__black_test__comments5_py.snap | 279 ++++++ ..._test__comments_non_breaking_space_py.snap | 126 +++ ...atter__tests__black_test__comments_py.snap | 322 ++++++ ...sts__black_test__docstring_preview_py.snap | 225 +++++ ...tter__tests__black_test__docstring_py.snap | 925 ++++++++++++++++++ ...atter__tests__black_test__fmtonoff_py.snap | 898 +++++++++++++++++ ...atter__tests__black_test__fmtskip8_py.snap | 293 ++++++ 9 files changed, 3282 insertions(+), 7 deletions(-) create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 6d8f6839b8..bffc9892e5 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -75,13 +75,7 @@ mod tests { pattern = "resources/test/fixtures/black/**/*.py", // Excluded tests because they reach unreachable when attaching tokens exclude = [ - "*comments.py", - "*comments[3,5,8].py", - "*comments_non_breaking_space.py", - "*docstring_preview.py", - "*docstring.py", - "*fmtonoff.py", - "*fmtskip8.py", + "*comments8.py", ]) ] #[test] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap new file mode 100644 index 0000000000..3e83a68a73 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap @@ -0,0 +1,213 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments3.py +--- +## Input + +```py +# The percent-percent comments are Spyder IDE cells. + + +# %% +def func(): + x = """ + a really long string + """ + lcomp3 = [ + # This one is actually too long to fit in a single line. + element.split("\n", 1)[0] + # yup + for element in collection.select_elements() + # right + if element is not None + ] + # Capture each of the exceptions in the MultiError along with each of their causes and contexts + if isinstance(exc_value, MultiError): + embedded = [] + for exc in exc_value.exceptions: + if exc not in _seen: + embedded.append( + # This should be left alone (before) + traceback.TracebackException.from_exception( + exc, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + # copy the set of _seen exceptions so that duplicates + # shared between sub-exceptions are not omitted + _seen=set(_seen), + ) + # This should be left alone (after) + ) + + # everything is fine if the expression isn't nested + traceback.TracebackException.from_exception( + exc, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + # copy the set of _seen exceptions so that duplicates + # shared between sub-exceptions are not omitted + _seen=set(_seen), + ) + + +# %% +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -9,10 +9,11 @@ + lcomp3 = [ + # This one is actually too long to fit in a single line. + element.split("\n", 1)[0] +- # yup +- for element in collection.select_elements() +- # right +- if element is not None ++ for # yup ++ element in collection.select_elements() ++ if # right ++ element ++ is not None + ] + # Capture each of the exceptions in the MultiError along with each of their causes and contexts + if isinstance(exc_value, MultiError): +@@ -26,9 +27,9 @@ + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, +- # copy the set of _seen exceptions so that duplicates ++ _seen=# copy the set of _seen exceptions so that duplicates + # shared between sub-exceptions are not omitted +- _seen=set(_seen), ++ set(_seen), + ) + # This should be left alone (after) + ) +@@ -39,9 +40,9 @@ + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, +- # copy the set of _seen exceptions so that duplicates ++ _seen=# copy the set of _seen exceptions so that duplicates + # shared between sub-exceptions are not omitted +- _seen=set(_seen), ++ set(_seen), + ) + + +``` + +## Ruff Output + +```py +# The percent-percent comments are Spyder IDE cells. + + +# %% +def func(): + x = """ + a really long string + """ + lcomp3 = [ + # This one is actually too long to fit in a single line. + element.split("\n", 1)[0] + for # yup + element in collection.select_elements() + if # right + element + is not None + ] + # Capture each of the exceptions in the MultiError along with each of their causes and contexts + if isinstance(exc_value, MultiError): + embedded = [] + for exc in exc_value.exceptions: + if exc not in _seen: + embedded.append( + # This should be left alone (before) + traceback.TracebackException.from_exception( + exc, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + _seen=# copy the set of _seen exceptions so that duplicates + # shared between sub-exceptions are not omitted + set(_seen), + ) + # This should be left alone (after) + ) + + # everything is fine if the expression isn't nested + traceback.TracebackException.from_exception( + exc, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + _seen=# copy the set of _seen exceptions so that duplicates + # shared between sub-exceptions are not omitted + set(_seen), + ) + + +# %% +``` + +## Black Output + +```py +# The percent-percent comments are Spyder IDE cells. + + +# %% +def func(): + x = """ + a really long string + """ + lcomp3 = [ + # This one is actually too long to fit in a single line. + element.split("\n", 1)[0] + # yup + for element in collection.select_elements() + # right + if element is not None + ] + # Capture each of the exceptions in the MultiError along with each of their causes and contexts + if isinstance(exc_value, MultiError): + embedded = [] + for exc in exc_value.exceptions: + if exc not in _seen: + embedded.append( + # This should be left alone (before) + traceback.TracebackException.from_exception( + exc, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + # copy the set of _seen exceptions so that duplicates + # shared between sub-exceptions are not omitted + _seen=set(_seen), + ) + # This should be left alone (after) + ) + + # everything is fine if the expression isn't nested + traceback.TracebackException.from_exception( + exc, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + # copy the set of _seen exceptions so that duplicates + # shared between sub-exceptions are not omitted + _seen=set(_seen), + ) + + +# %% +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap new file mode 100644 index 0000000000..32a6cb310c --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap @@ -0,0 +1,279 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments5.py +--- +## Input + +```py +while True: + if something.changed: + do.stuff() # trailing comment + # Comment belongs to the `if` block. + # This one belongs to the `while` block. + + # Should this one, too? I guess so. + +# This one is properly standalone now. + +for i in range(100): + # first we do this + if i % 33 == 0: + break + + # then we do this + print(i) + # and finally we loop around + +with open(some_temp_file) as f: + data = f.read() + +try: + with open(some_other_file) as w: + w.write(data) + +except OSError: + print("problems") + +import sys + + +# leading function comment +def wat(): + ... + # trailing function comment + + +# SECTION COMMENT + + +# leading 1 +@deco1 +# leading 2 +@deco2(with_args=True) +# leading 3 +@deco3 +def decorated1(): + ... + + +# leading 1 +@deco1 +# leading 2 +@deco2(with_args=True) +# leading function comment +def decorated1(): + ... + + +# Note: this is fixed in +# Preview.empty_lines_before_class_or_def_with_leading_comments. +# In the current style, the user will have to split those lines by hand. +some_instruction + + +# This comment should be split from `some_instruction` by two lines but isn't. +def g(): + ... + + +if __name__ == "__main__": + main() +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -23,7 +23,6 @@ + try: + with open(some_other_file) as w: + w.write(data) +- + except OSError: + print("problems") + +@@ -41,18 +40,18 @@ + + # leading 1 + @deco1 +-# leading 2 +-@deco2(with_args=True) +-# leading 3 +-@deco3 ++@# leading 2 ++deco2(with_args=True) ++@# leading 3 ++deco3 + def decorated1(): + ... + + + # leading 1 + @deco1 +-# leading 2 +-@deco2(with_args=True) ++@# leading 2 ++deco2(with_args=True) + # leading function comment + def decorated1(): + ... +``` + +## Ruff Output + +```py +while True: + if something.changed: + do.stuff() # trailing comment + # Comment belongs to the `if` block. + # This one belongs to the `while` block. + + # Should this one, too? I guess so. + +# This one is properly standalone now. + +for i in range(100): + # first we do this + if i % 33 == 0: + break + + # then we do this + print(i) + # and finally we loop around + +with open(some_temp_file) as f: + data = f.read() + +try: + with open(some_other_file) as w: + w.write(data) +except OSError: + print("problems") + +import sys + + +# leading function comment +def wat(): + ... + # trailing function comment + + +# SECTION COMMENT + + +# leading 1 +@deco1 +@# leading 2 +deco2(with_args=True) +@# leading 3 +deco3 +def decorated1(): + ... + + +# leading 1 +@deco1 +@# leading 2 +deco2(with_args=True) +# leading function comment +def decorated1(): + ... + + +# Note: this is fixed in +# Preview.empty_lines_before_class_or_def_with_leading_comments. +# In the current style, the user will have to split those lines by hand. +some_instruction + + +# This comment should be split from `some_instruction` by two lines but isn't. +def g(): + ... + + +if __name__ == "__main__": + main() +``` + +## Black Output + +```py +while True: + if something.changed: + do.stuff() # trailing comment + # Comment belongs to the `if` block. + # This one belongs to the `while` block. + + # Should this one, too? I guess so. + +# This one is properly standalone now. + +for i in range(100): + # first we do this + if i % 33 == 0: + break + + # then we do this + print(i) + # and finally we loop around + +with open(some_temp_file) as f: + data = f.read() + +try: + with open(some_other_file) as w: + w.write(data) + +except OSError: + print("problems") + +import sys + + +# leading function comment +def wat(): + ... + # trailing function comment + + +# SECTION COMMENT + + +# leading 1 +@deco1 +# leading 2 +@deco2(with_args=True) +# leading 3 +@deco3 +def decorated1(): + ... + + +# leading 1 +@deco1 +# leading 2 +@deco2(with_args=True) +# leading function comment +def decorated1(): + ... + + +# Note: this is fixed in +# Preview.empty_lines_before_class_or_def_with_leading_comments. +# In the current style, the user will have to split those lines by hand. +some_instruction + + +# This comment should be split from `some_instruction` by two lines but isn't. +def g(): + ... + + +if __name__ == "__main__": + main() +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap new file mode 100644 index 0000000000..7c88e6c3a2 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap @@ -0,0 +1,126 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments_non_breaking_space.py +--- +## Input + +```py +from .config import ( ConfigTypeAttributes, Int, Path, # String, + # DEFAULT_TYPE_ATTRIBUTES, +) + +result = 1 # A simple comment +result = ( 1, ) # Another one + +result = 1 # type: ignore +result = 1# This comment is talking about type: ignore +square = Square(4) # type: Optional[Square] + +def function(a:int=42): + """ This docstring is already formatted + a + b + """ + #  There's a NBSP + 3 spaces before + # And 4 spaces on the next line + pass +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,23 +1,22 @@ + from .config import ( + ConfigTypeAttributes, + Int, +- Path, # String, +- # DEFAULT_TYPE_ATTRIBUTES, ++ Path, # String, + ) + +-result = 1 # A simple comment +-result = (1,) # Another one ++result = 1 # A simple comment ++result = (1,) # Another one + +-result = 1 #  type: ignore +-result = 1 # This comment is talking about type: ignore +-square = Square(4) #  type: Optional[Square] ++result = 1 # type: ignore ++result = 1 # This comment is talking about type: ignore ++square = Square(4) # type: Optional[Square] + + + def function(a: int = 42): +- """This docstring is already formatted +- a +- b ++ """ This docstring is already formatted ++ a ++ b + """ +- # There's a NBSP + 3 spaces before ++ #  There's a NBSP + 3 spaces before + # And 4 spaces on the next line + pass +``` + +## Ruff Output + +```py +from .config import ( + ConfigTypeAttributes, + Int, + Path, # String, +) + +result = 1 # A simple comment +result = (1,) # Another one + +result = 1 # type: ignore +result = 1 # This comment is talking about type: ignore +square = Square(4) # type: Optional[Square] + + +def function(a: int = 42): + """ This docstring is already formatted + a + b + """ + #  There's a NBSP + 3 spaces before + # And 4 spaces on the next line + pass +``` + +## Black Output + +```py +from .config import ( + ConfigTypeAttributes, + Int, + Path, # String, + # DEFAULT_TYPE_ATTRIBUTES, +) + +result = 1 # A simple comment +result = (1,) # Another one + +result = 1 #  type: ignore +result = 1 # This comment is talking about type: ignore +square = Square(4) #  type: Optional[Square] + + +def function(a: int = 42): + """This docstring is already formatted + a + b + """ + # There's a NBSP + 3 spaces before + # And 4 spaces on the next line + pass +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap new file mode 100644 index 0000000000..b28ec30126 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap @@ -0,0 +1,322 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py +--- +## Input + +```py +#!/usr/bin/env python3 +# fmt: on +# Some license here. +# +# Has many lines. Many, many lines. +# Many, many, many lines. +"""Module docstring. + +Possibly also many, many lines. +""" + +import os.path +import sys + +import a +from b.c import X # some noqa comment + +try: + import fast +except ImportError: + import slow as fast + + +# Some comment before a function. +y = 1 +( + # some strings + y # type: ignore +) + + +def function(default=None): + """Docstring comes first. + + Possibly many lines. + """ + # FIXME: Some comment about why this function is crap but still in production. + import inner_imports + + if inner_imports.are_evil(): + # Explains why we have this if. + # In great detail indeed. + x = X() + return x.method1() # type: ignore + + # This return is also commented for some reason. + return default + + +# Explains why we use global state. +GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} + + +# Another comment! +# This time two lines. + + +class Foo: + """Docstring for class Foo. Example from Sphinx docs.""" + + #: Doc comment for class attribute Foo.bar. + #: It can have multiple lines. + bar = 1 + + flox = 1.5 #: Doc comment for Foo.flox. One line only. + + baz = 2 + """Docstring for class attribute Foo.baz.""" + + def __init__(self): + #: Doc comment for instance attribute qux. + self.qux = 3 + + self.spam = 4 + """Docstring for instance attribute spam.""" + + +#'

This is pweave!

+ + +@fast(really=True) +async def wat(): + # This comment, for some reason \ + # contains a trailing backslash. + async with X.open_async() as x: # Some more comments + result = await x.method1() + # Comment after ending a block. + if result: + print("A OK", file=sys.stdout) + # Comment between things. + print() + + +# Some closing comments. +# Maybe Vim or Emacs directives for formatting. +# Who knows. +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -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 + +```py +#!/usr/bin/env python3 +# fmt: on +# Some license here. +# +# Has many lines. Many, many lines. +# Many, many, many lines. +"""Module docstring. + +Possibly also many, many lines. +""" + +import os.path +import sys + +import a +from b.c import X # some noqa comment + +try: + import fast +except ImportError: + import slow as fast + + +# Some comment before a function. +y = 1 +( + # some strings + y # type: ignore +) + + +def function(default=None): + """Docstring comes first. + + Possibly many lines. + """ + # FIXME: Some comment about why this function is crap but still in production. + import inner_imports + + if inner_imports.are_evil(): + # Explains why we have this if. + # In great detail indeed. + x = X() + return x.method1() # type: ignore + + # This return is also commented for some reason. + return default + + +# Explains why we use global state. +GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} + + +# Another comment! +# This time two lines. + + +class Foo: + """Docstring for class Foo. Example from Sphinx docs.""" + + #: Doc comment for class attribute Foo.bar. + #: It can have multiple lines. + bar = 1 + + flox = 1.5 #: Doc comment for Foo.flox. One line only. + + baz = 2 + """Docstring for class attribute Foo.baz.""" + + def __init__(self): + #: Doc comment for instance attribute qux. + self.qux = 3 + + self.spam = 4 + """Docstring for instance attribute spam.""" + + +#'

This is pweave!

+ + +@fast(really=True) +async def wat(): + # This comment, for some reason \ + # contains a trailing backslash. + async with X.open_async() as x: # Some more comments + result = await x.method1() + # Comment after ending a block. + if result: + print("A OK", file=sys.stdout) + # Comment between things. + print() + + +# Some closing comments. +# Maybe Vim or Emacs directives for formatting. +# Who knows. +``` + +## Black Output + +```py +#!/usr/bin/env python3 +# fmt: on +# Some license here. +# +# Has many lines. Many, many lines. +# Many, many, many lines. +"""Module docstring. + +Possibly also many, many lines. +""" + +import os.path +import sys + +import a +from b.c import X # some noqa comment + +try: + import fast +except ImportError: + import slow as fast + + +# Some comment before a function. +y = 1 +( + # some strings + y # type: ignore +) + + +def function(default=None): + """Docstring comes first. + + Possibly many lines. + """ + # FIXME: Some comment about why this function is crap but still in production. + import inner_imports + + if inner_imports.are_evil(): + # Explains why we have this if. + # In great detail indeed. + x = X() + return x.method1() # type: ignore + + # This return is also commented for some reason. + return default + + +# Explains why we use global state. +GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} + + +# Another comment! +# This time two lines. + + +class Foo: + """Docstring for class Foo. Example from Sphinx docs.""" + + #: Doc comment for class attribute Foo.bar. + #: It can have multiple lines. + bar = 1 + + flox = 1.5 #: Doc comment for Foo.flox. One line only. + + baz = 2 + """Docstring for class attribute Foo.baz.""" + + def __init__(self): + #: Doc comment for instance attribute qux. + self.qux = 3 + + self.spam = 4 + """Docstring for instance attribute spam.""" + + +#'

This is pweave!

+ + +@fast(really=True) +async def wat(): + # This comment, for some reason \ + # contains a trailing backslash. + async with X.open_async() as x: # Some more comments + result = await x.method1() + # Comment after ending a block. + if result: + print("A OK", file=sys.stdout) + # Comment between things. + print() + + +# Some closing comments. +# Maybe Vim or Emacs directives for formatting. +# Who knows.``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap new file mode 100644 index 0000000000..fed3568a34 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap @@ -0,0 +1,225 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/docstring_preview.py +--- +## Input + +```py +def docstring_almost_at_line_limit(): + """long docstring................................................................. + """ + + +def docstring_almost_at_line_limit_with_prefix(): + f"""long docstring................................................................ + """ + + +def mulitline_docstring_almost_at_line_limit(): + """long docstring................................................................. + + .................................................................................. + """ + + +def mulitline_docstring_almost_at_line_limit_with_prefix(): + f"""long docstring................................................................ + + .................................................................................. + """ + + +def docstring_at_line_limit(): + """long docstring................................................................""" + + +def docstring_at_line_limit_with_prefix(): + f"""long docstring...............................................................""" + + +def multiline_docstring_at_line_limit(): + """first line----------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" + + +def multiline_docstring_at_line_limit_with_prefix(): + f"""first line---------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" + + +def single_quote_docstring_over_line_limit(): + "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." + + +def single_quote_docstring_over_line_limit2(): + 'We do not want to put the closing quote on a new line as that is invalid (see GH-3141).' +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,9 +1,13 @@ + def docstring_almost_at_line_limit(): +- """long docstring.................................................................""" ++ """long docstring................................................................. ++ """ + + + def docstring_almost_at_line_limit_with_prefix(): +- f"""long docstring................................................................""" ++ ( ++ f"""long docstring................................................................ ++ """ ++ ) + + + def mulitline_docstring_almost_at_line_limit(): +@@ -14,10 +18,12 @@ + + + def mulitline_docstring_almost_at_line_limit_with_prefix(): +- f"""long docstring................................................................ ++ ( ++ f"""long docstring................................................................ + + .................................................................................. + """ ++ ) + + + def docstring_at_line_limit(): +@@ -35,9 +41,11 @@ + + + def multiline_docstring_at_line_limit_with_prefix(): +- f"""first line---------------------------------------------------------------------- ++ ( ++ f"""first line---------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" ++ ) + + + def single_quote_docstring_over_line_limit(): +``` + +## Ruff Output + +```py +def docstring_almost_at_line_limit(): + """long docstring................................................................. + """ + + +def docstring_almost_at_line_limit_with_prefix(): + ( + f"""long docstring................................................................ + """ + ) + + +def mulitline_docstring_almost_at_line_limit(): + """long docstring................................................................. + + .................................................................................. + """ + + +def mulitline_docstring_almost_at_line_limit_with_prefix(): + ( + f"""long docstring................................................................ + + .................................................................................. + """ + ) + + +def docstring_at_line_limit(): + """long docstring................................................................""" + + +def docstring_at_line_limit_with_prefix(): + f"""long docstring...............................................................""" + + +def multiline_docstring_at_line_limit(): + """first line----------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" + + +def multiline_docstring_at_line_limit_with_prefix(): + ( + f"""first line---------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" + ) + + +def single_quote_docstring_over_line_limit(): + "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." + + +def single_quote_docstring_over_line_limit2(): + "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." +``` + +## Black Output + +```py +def docstring_almost_at_line_limit(): + """long docstring.................................................................""" + + +def docstring_almost_at_line_limit_with_prefix(): + f"""long docstring................................................................""" + + +def mulitline_docstring_almost_at_line_limit(): + """long docstring................................................................. + + .................................................................................. + """ + + +def mulitline_docstring_almost_at_line_limit_with_prefix(): + f"""long docstring................................................................ + + .................................................................................. + """ + + +def docstring_at_line_limit(): + """long docstring................................................................""" + + +def docstring_at_line_limit_with_prefix(): + f"""long docstring...............................................................""" + + +def multiline_docstring_at_line_limit(): + """first line----------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" + + +def multiline_docstring_at_line_limit_with_prefix(): + f"""first line---------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" + + +def single_quote_docstring_over_line_limit(): + "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." + + +def single_quote_docstring_over_line_limit2(): + "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap new file mode 100644 index 0000000000..da177f0353 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap @@ -0,0 +1,925 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/docstring.py +--- +## Input + +```py +class MyClass: + """ Multiline + class docstring + """ + + def method(self): + """Multiline + method docstring + """ + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def bar(): + '''This is another docstring + with more lines of text + ''' + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def troz(): + '''Indentation with tabs + is just as OK + ''' + return + + +def zort(): + """Another + multiline + docstring + """ + pass + +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 multiline_whitespace(): + ''' + + + + + ''' + + +def oneline_whitespace(): + ''' ''' + + +def empty(): + """""" + + +def single_quotes(): + 'testing' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): ''' +"hey yah"''' + + +def ignored_docstring(): + """a => \ +b""" + +def single_line_docstring_with_whitespace(): + """ This should be stripped """ + +def docstring_with_inline_tabs_and_space_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + + +def docstring_with_inline_tabs_and_tab_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + pass + + +def backslash_space(): + """\ """ + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + +# Regression test for #3425 +def multiline_backslash_really_long_dont_crash(): + """ + hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ + + +def multiline_backslash_3(): + ''' + already escaped \\ ''' + + +def my_god_its_full_of_stars_1(): + "I'm sorry Dave\u2001" + + +# the space below is actually a \u2001, removed in output +def my_god_its_full_of_stars_2(): + "I'm sorry Dave " + + +def docstring_almost_at_line_limit(): + """long docstring.................................................................""" + + +def docstring_almost_at_line_limit2(): + """long docstring................................................................. + + .................................................................................. + """ + + +def docstring_at_line_limit(): + """long docstring................................................................""" + + +def multiline_docstring_at_line_limit(): + """first line----------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" + + +def stable_quote_normalization_with_immediate_inner_single_quote(self): + '''' + + + ''' +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,83 +1,85 @@ + class MyClass: +- """Multiline +- class docstring +- """ ++ """ Multiline ++ class docstring ++ """ + + def method(self): + """Multiline +- method docstring +- """ ++ method 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 bar(): + """This is another docstring +- with more lines of text +- """ ++ with more lines of text ++ """ + return + + + def baz(): + '''"This" is a string with some +- embedded "quotes"''' ++ embedded "quotes"''' + return + + + def troz(): + """Indentation with tabs +- is just as OK +- """ ++ is just as OK ++ """ + return + + + def zort(): + """Another +- multiline +- docstring +- """ ++ multiline ++ docstring ++ """ + pass + + + 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 + + +@@ -93,20 +95,25 @@ + + def and_that(): + """ +- "hey yah" """ ++ "hey yah" """ + + + def and_this(): +- ''' +- "hey yah"''' ++ ''' ++ "hey yah"''' + + + def multiline_whitespace(): +- """ """ ++ """ ++ ++ ++ ++ ++ """ + + + def oneline_whitespace(): +- """ """ ++ """ """ + + + def empty(): +@@ -118,8 +125,8 @@ + + + def believe_it_or_not_this_is_in_the_py_stdlib(): +- ''' +- "hey yah"''' ++ ''' ++"hey yah"''' + + + def ignored_docstring(): +@@ -128,31 +135,31 @@ + + + def single_line_docstring_with_whitespace(): +- """This should be stripped""" ++ """ This should be stripped """ + + + def docstring_with_inline_tabs_and_space_indentation(): + """hey + + tab separated value +- tab at start of line and then a tab separated value +- multiple tabs at the beginning and inline +- mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. +- +- line ends with some tabs ++ tab at start of line and then a tab separated value ++ multiple tabs at the beginning and inline ++ mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. ++ ++ line ends with some tabs + """ + + + def docstring_with_inline_tabs_and_tab_indentation(): + """hey + +- tab separated value +- tab at start of line and then a tab separated value +- multiple tabs at the beginning and inline +- mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. +- +- line ends with some tabs +- """ ++ tab separated value ++ tab at start of line and then a tab separated value ++ multiple tabs at the beginning and inline ++ mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. ++ ++ line ends with some tabs ++ """ + pass + + +@@ -168,7 +175,7 @@ + + def multiline_backslash_2(): + """ +- hey there \ """ ++ hey there \ """ + + + # Regression test for #3425 +@@ -179,7 +186,7 @@ + + def multiline_backslash_3(): + """ +- already escaped \\""" ++ already escaped \\ """ + + + def my_god_its_full_of_stars_1(): +@@ -188,7 +195,7 @@ + + # the space below is actually a \u2001, removed in output + def my_god_its_full_of_stars_2(): +- "I'm sorry Dave" ++ "I'm sorry Dave " + + + def docstring_almost_at_line_limit(): +``` + +## Ruff Output + +```py +class MyClass: + """ Multiline + class docstring + """ + + def method(self): + """Multiline + method docstring + """ + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def bar(): + """This is another docstring + with more lines of text + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def troz(): + """Indentation with tabs + is just as OK + """ + return + + +def zort(): + """Another + multiline + docstring + """ + pass + + +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 multiline_whitespace(): + """ + + + + + """ + + +def oneline_whitespace(): + """ """ + + +def empty(): + """""" + + +def single_quotes(): + "testing" + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + ''' +"hey yah"''' + + +def ignored_docstring(): + """a => \ +b""" + + +def single_line_docstring_with_whitespace(): + """ This should be stripped """ + + +def docstring_with_inline_tabs_and_space_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + + +def docstring_with_inline_tabs_and_tab_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + pass + + +def backslash_space(): + """\ """ + + +def multiline_backslash_1(): + """ + hey\there\ + \ """ + + +def multiline_backslash_2(): + """ + hey there \ """ + + +# Regression test for #3425 +def multiline_backslash_really_long_dont_crash(): + """ + hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ + + +def multiline_backslash_3(): + """ + already escaped \\ """ + + +def my_god_its_full_of_stars_1(): + "I'm sorry Dave\u2001" + + +# the space below is actually a \u2001, removed in output +def my_god_its_full_of_stars_2(): + "I'm sorry Dave " + + +def docstring_almost_at_line_limit(): + """long docstring.................................................................""" + + +def docstring_almost_at_line_limit2(): + """long docstring................................................................. + + .................................................................................. + """ + + +def docstring_at_line_limit(): + """long docstring................................................................""" + + +def multiline_docstring_at_line_limit(): + """first line----------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" + + +def stable_quote_normalization_with_immediate_inner_single_quote(self): + """' + + + """ +``` + +## Black Output + +```py +class MyClass: + """Multiline + class docstring + """ + + def method(self): + """Multiline + method docstring + """ + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def bar(): + """This is another docstring + with more lines of text + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def troz(): + """Indentation with tabs + is just as OK + """ + return + + +def zort(): + """Another + multiline + docstring + """ + pass + + +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 multiline_whitespace(): + """ """ + + +def oneline_whitespace(): + """ """ + + +def empty(): + """""" + + +def single_quotes(): + "testing" + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + ''' + "hey yah"''' + + +def ignored_docstring(): + """a => \ +b""" + + +def single_line_docstring_with_whitespace(): + """This should be stripped""" + + +def docstring_with_inline_tabs_and_space_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + + +def docstring_with_inline_tabs_and_tab_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + pass + + +def backslash_space(): + """\ """ + + +def multiline_backslash_1(): + """ + hey\there\ + \ """ + + +def multiline_backslash_2(): + """ + hey there \ """ + + +# Regression test for #3425 +def multiline_backslash_really_long_dont_crash(): + """ + hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ + + +def multiline_backslash_3(): + """ + already escaped \\""" + + +def my_god_its_full_of_stars_1(): + "I'm sorry Dave\u2001" + + +# the space below is actually a \u2001, removed in output +def my_god_its_full_of_stars_2(): + "I'm sorry Dave" + + +def docstring_almost_at_line_limit(): + """long docstring.................................................................""" + + +def docstring_almost_at_line_limit2(): + """long docstring................................................................. + + .................................................................................. + """ + + +def docstring_at_line_limit(): + """long docstring................................................................""" + + +def multiline_docstring_at_line_limit(): + """first line----------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" + + +def stable_quote_normalization_with_immediate_inner_single_quote(self): + """' + + + """ +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap new file mode 100644 index 0000000000..8206dd5719 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap @@ -0,0 +1,898 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py +--- +## Input + +```py +#!/usr/bin/env python3 +import asyncio +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) +# fmt: on +f'trigger 3.6 mode' +# Comment 1 + +# Comment 2 + +# fmt: off +def func_no_args(): + a; b; c + if True: raise RuntimeError + if False: ... + for i in range(10): + print(i) + continue + exec('new-style exec', {}, {}) + return None +async def coroutine(arg, exec=False): + 'Single-line docstring. Multiline is harder to reformat.' + async with some_connection() as conn: + await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) + await asyncio.sleep(1) +@asyncio.coroutine +@some_decorator( +with_args=True, +many_args=[1,2,3] +) +def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str: + return text[number:-1] +# fmt: on +def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''): + offset = attr.ib(default=attr.Factory( lambda: _r.uniform(1, 2))) + assert task._cancel_stack[:len(old_stack)] == old_stack +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)): + ... +something = { + # fmt: off + key: 'value', +} + +def subscriptlist(): + atom[ + # fmt: off + 'some big and', + 'complex subscript', + # fmt: on + goes + here, andhere, + ] + +def import_as_names(): + # fmt: off + from hello import a, b + 'unformatted' + # fmt: on + +def testlist_star_expr(): + # fmt: off + a , b = *hello + 'unformatted' + # fmt: on + +def yield_expr(): + # fmt: off + yield hello + 'unformatted' + # fmt: on + 'formatted' + # fmt: off + ( yield hello ) + 'unformatted' + # fmt: on + +def example(session): + # fmt: off + result = session\ + .query(models.Customer.id)\ + .filter(models.Customer.account_id == account_id, + models.Customer.email == email_address)\ + .order_by(models.Customer.id.asc())\ + .all() + # fmt: on +def off_and_on_without_data(): + """All comments here are technically on the same prefix. + + The comments between will be formatted. This is a known limitation. + """ + # fmt: off + + + #hey, that won't work + + + # fmt: on + pass +def on_and_off_broken(): + """Another known limitation.""" + # fmt: on + # fmt: off + this=should.not_be.formatted() + and_=indeed . it is not formatted + because . the . handling . inside . generate_ignored_nodes() + now . considers . multiple . fmt . directives . within . one . prefix + # fmt: on + # fmt: off + # ...but comments still get reformatted even though they should not be + # fmt: on +def long_lines(): + if True: + typedargslist.extend( + gen_annotated_params(ast_args.kwonlyargs, ast_args.kw_defaults, parameters, implicit_default=True) + ) + # fmt: off + a = ( + unnecessary_bracket() + ) + # fmt: on + _type_comment_re = re.compile( + r""" + ^ + [\t ]* + \#[ ]type:[ ]* + (?P + [^#\t\n]+? + ) + (? to match + # a trailing space which is why we need the silliness below + (? + (?:\#[^\n]*)? + \n? + ) + $ + """, + # fmt: off + re.MULTILINE|re.VERBOSE + # fmt: on + ) +def single_literal_yapf_disable(): + """Black does not support this.""" + BAZ = { + (1, 2, 3, 4), + (5, 6, 7, 8), + (9, 10, 11, 12) + } # yapf: disable +cfg.rule( + "Default", "address", + xxxx_xxxx=["xxx-xxxxxx-xxxxxxxxxx"], + xxxxxx="xx_xxxxx", xxxxxxx="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + xxxxxxxxx_xxxx=True, xxxxxxxx_xxxxxxxxxx=False, + xxxxxx_xxxxxx=2, xxxxxx_xxxxx_xxxxxxxx=70, xxxxxx_xxxxxx_xxxxx=True, + # fmt: off + xxxxxxx_xxxxxxxxxxxx={ + "xxxxxxxx": { + "xxxxxx": False, + "xxxxxxx": False, + "xxxx_xxxxxx": "xxxxx", + }, + "xxxxxxxx-xxxxx": { + "xxxxxx": False, + "xxxxxxx": True, + "xxxx_xxxxxx": "xxxxxx", + }, + }, + # fmt: on + xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5 +) +# fmt: off +yield 'hello' +# No formatting to the end of the file +l=[1,2,3] +d={'a':1, + 'b':2} +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -5,39 +5,53 @@ + from third_party import X, Y, Z + + from library import some_connection, some_decorator ++# fmt: off ++from third_party import X, Y, Z + +-# fmt: off +-from third_party import (X, +- Y, Z) + # fmt: on +-f"trigger 3.6 mode" ++f'trigger 3.6 mode' ++ ++ + # Comment 1 + + # Comment 2 +- + + # fmt: off + def func_no_args(): +- a; b; c +- if True: raise RuntimeError +- if False: ... +- for i in range(10): +- print(i) +- continue +- exec('new-style exec', {}, {}) +- return None ++ a ++ b ++ c ++ if True: ++ raise RuntimeError ++ if False: ++ ... ++ for i in range(10): ++ print(i) ++ continue ++ exec("new-style exec", {}, {}) ++ return None ++ ++ + async def coroutine(arg, exec=False): +- 'Single-line docstring. Multiline is harder to reformat.' +- async with some_connection() as conn: +- await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) +- await asyncio.sleep(1) ++ "Single-line docstring. Multiline is harder to reformat." ++ async with some_connection() as conn: ++ await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) ++ await asyncio.sleep(1) ++ ++ + @asyncio.coroutine +-@some_decorator( +-with_args=True, +-many_args=[1,2,3] +-) +-def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str: +- return text[number:-1] ++@some_decorator(with_args=True, many_args=[1, 2, 3]) ++def function_signature_stress_test( ++ number: int, ++ no_annotation=None, ++ text: str = "default", ++ *, ++ debug: bool = False, ++ **kwargs, ++) -> str: ++ return text[number:-1] ++ ++ + # fmt: on + def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): + offset = attr.ib(default=attr.Factory(lambda: _r.uniform(1, 2))) +@@ -64,55 +78,55 @@ + + something = { + # fmt: off +- key: 'value', ++ key: "value", + } + + + def subscriptlist(): + atom[ + # fmt: off +- 'some big and', +- 'complex subscript', ++ "some big and", ++ "complex subscript", + # fmt: on +- goes + here, ++ goes ++ + here, + andhere, + ] + + + def import_as_names(): + # fmt: off +- from hello import a, b +- 'unformatted' ++ from hello import a, b ++ ++ "unformatted" + # fmt: on + + + def testlist_star_expr(): + # fmt: off +- a , b = *hello +- 'unformatted' ++ a, b = *hello ++ "unformatted" + # fmt: on + + + def yield_expr(): + # fmt: off + yield hello +- 'unformatted' ++ "unformatted" + # fmt: on + "formatted" + # fmt: off +- ( yield hello ) +- 'unformatted' ++ (yield hello) ++ "unformatted" + # fmt: on + + + def example(session): + # fmt: off +- result = session\ +- .query(models.Customer.id)\ +- .filter(models.Customer.account_id == account_id, +- models.Customer.email == email_address)\ +- .order_by(models.Customer.id.asc())\ +- .all() ++ result = session.query(models.Customer.id).filter( ++ models.Customer.account_id == account_id, ++ models.Customer.email == email_address, ++ ).order_by(models.Customer.id.asc()).all() + # fmt: on + + +@@ -123,7 +137,7 @@ + """ + # fmt: off + +- # hey, that won't work ++ #hey, that won't work + + # fmt: on + pass +@@ -133,10 +147,10 @@ + """Another known limitation.""" + # fmt: on + # fmt: off +- this=should.not_be.formatted() +- and_=indeed . it is not formatted +- because . the . handling . inside . generate_ignored_nodes() +- now . considers . multiple . fmt . directives . within . one . prefix ++ this = should.not_be.formatted() ++ and_ = indeed.it is not formatted ++ because.the.handling.inside.generate_ignored_nodes() ++ now.considers.multiple.fmt.directives.within.one.prefix + # fmt: on + # fmt: off + # ...but comments still get reformatted even though they should not be +@@ -154,9 +168,7 @@ + ) + ) + # fmt: off +- a = ( +- unnecessary_bracket() +- ) ++ a = unnecessary_bracket() + # fmt: on + _type_comment_re = re.compile( + r""" +@@ -179,7 +191,8 @@ + $ + """, + # fmt: off +- re.MULTILINE|re.VERBOSE ++ re.MULTILINE ++ | re.VERBOSE, + # fmt: on + ) + +@@ -200,8 +213,8 @@ + xxxxxx_xxxxxx=2, + xxxxxx_xxxxx_xxxxxxxx=70, + xxxxxx_xxxxxx_xxxxx=True, +- # fmt: off +- xxxxxxx_xxxxxxxxxxxx={ ++ xxxxxxx_xxxxxxxxxxxx=# fmt: off ++ { + "xxxxxxxx": { + "xxxxxx": False, + "xxxxxxx": False, +@@ -213,12 +226,11 @@ + "xxxx_xxxxxx": "xxxxxx", + }, + }, +- # fmt: on +- xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, ++ xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=# fmt: on ++ 5, + ) + # fmt: off +-yield 'hello' ++yield "hello" + # No formatting to the end of the file +-l=[1,2,3] +-d={'a':1, +- 'b':2} ++l = [1, 2, 3] ++d = {"a": 1, "b": 2} +``` + +## Ruff Output + +```py +#!/usr/bin/env python3 +import asyncio +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 + +# fmt: on +f'trigger 3.6 mode' + + +# Comment 1 + +# Comment 2 + +# fmt: off +def func_no_args(): + a + b + c + if True: + raise RuntimeError + if False: + ... + for i in range(10): + print(i) + continue + exec("new-style exec", {}, {}) + return None + + +async def coroutine(arg, exec=False): + "Single-line docstring. Multiline is harder to reformat." + async with some_connection() as conn: + await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) + await asyncio.sleep(1) + + +@asyncio.coroutine +@some_decorator(with_args=True, many_args=[1, 2, 3]) +def function_signature_stress_test( + number: int, + no_annotation=None, + text: str = "default", + *, + debug: bool = False, + **kwargs, +) -> str: + return text[number:-1] + + +# fmt: on +def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): + offset = attr.ib(default=attr.Factory(lambda: _r.uniform(1, 2))) + assert task._cancel_stack[: len(old_stack)] == old_stack + + +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)): + ... + + +something = { + # fmt: off + key: "value", +} + + +def subscriptlist(): + atom[ + # fmt: off + "some big and", + "complex subscript", + # fmt: on + goes + + here, + andhere, + ] + + +def import_as_names(): + # fmt: off + from hello import a, b + + "unformatted" + # fmt: on + + +def testlist_star_expr(): + # fmt: off + a, b = *hello + "unformatted" + # fmt: on + + +def yield_expr(): + # fmt: off + yield hello + "unformatted" + # fmt: on + "formatted" + # fmt: off + (yield hello) + "unformatted" + # fmt: on + + +def example(session): + # fmt: off + result = session.query(models.Customer.id).filter( + models.Customer.account_id == account_id, + models.Customer.email == email_address, + ).order_by(models.Customer.id.asc()).all() + # fmt: on + + +def off_and_on_without_data(): + """All comments here are technically on the same prefix. + + The comments between will be formatted. This is a known limitation. + """ + # fmt: off + + #hey, that won't work + + # fmt: on + pass + + +def on_and_off_broken(): + """Another known limitation.""" + # fmt: on + # fmt: off + this = should.not_be.formatted() + and_ = indeed.it is not formatted + because.the.handling.inside.generate_ignored_nodes() + now.considers.multiple.fmt.directives.within.one.prefix + # fmt: on + # fmt: off + # ...but comments still get reformatted even though they should not be + # fmt: on + + +def long_lines(): + if True: + typedargslist.extend( + gen_annotated_params( + ast_args.kwonlyargs, + ast_args.kw_defaults, + parameters, + implicit_default=True, + ) + ) + # fmt: off + a = unnecessary_bracket() + # fmt: on + _type_comment_re = re.compile( + r""" + ^ + [\t ]* + \#[ ]type:[ ]* + (?P + [^#\t\n]+? + ) + (? to match + # a trailing space which is why we need the silliness below + (? + (?:\#[^\n]*)? + \n? + ) + $ + """, + # fmt: off + re.MULTILINE + | re.VERBOSE, + # fmt: on + ) + + +def single_literal_yapf_disable(): + """Black does not support this.""" + BAZ = {(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)} # yapf: disable + + +cfg.rule( + "Default", + "address", + xxxx_xxxx=["xxx-xxxxxx-xxxxxxxxxx"], + xxxxxx="xx_xxxxx", + xxxxxxx="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + xxxxxxxxx_xxxx=True, + xxxxxxxx_xxxxxxxxxx=False, + xxxxxx_xxxxxx=2, + xxxxxx_xxxxx_xxxxxxxx=70, + xxxxxx_xxxxxx_xxxxx=True, + xxxxxxx_xxxxxxxxxxxx=# fmt: off + { + "xxxxxxxx": { + "xxxxxx": False, + "xxxxxxx": False, + "xxxx_xxxxxx": "xxxxx", + }, + "xxxxxxxx-xxxxx": { + "xxxxxx": False, + "xxxxxxx": True, + "xxxx_xxxxxx": "xxxxxx", + }, + }, + xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=# fmt: on + 5, +) +# fmt: off +yield "hello" +# No formatting to the end of the file +l = [1, 2, 3] +d = {"a": 1, "b": 2} +``` + +## Black Output + +```py +#!/usr/bin/env python3 +import asyncio +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) +# fmt: on +f"trigger 3.6 mode" +# Comment 1 + +# Comment 2 + + +# fmt: off +def func_no_args(): + a; b; c + if True: raise RuntimeError + if False: ... + for i in range(10): + print(i) + continue + exec('new-style exec', {}, {}) + return None +async def coroutine(arg, exec=False): + 'Single-line docstring. Multiline is harder to reformat.' + async with some_connection() as conn: + await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) + await asyncio.sleep(1) +@asyncio.coroutine +@some_decorator( +with_args=True, +many_args=[1,2,3] +) +def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str: + return text[number:-1] +# fmt: on +def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): + offset = attr.ib(default=attr.Factory(lambda: _r.uniform(1, 2))) + assert task._cancel_stack[: len(old_stack)] == old_stack + + +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)): + ... + + +something = { + # fmt: off + key: 'value', +} + + +def subscriptlist(): + atom[ + # fmt: off + 'some big and', + 'complex subscript', + # fmt: on + goes + here, + andhere, + ] + + +def import_as_names(): + # fmt: off + from hello import a, b + 'unformatted' + # fmt: on + + +def testlist_star_expr(): + # fmt: off + a , b = *hello + 'unformatted' + # fmt: on + + +def yield_expr(): + # fmt: off + yield hello + 'unformatted' + # fmt: on + "formatted" + # fmt: off + ( yield hello ) + 'unformatted' + # fmt: on + + +def example(session): + # fmt: off + result = session\ + .query(models.Customer.id)\ + .filter(models.Customer.account_id == account_id, + models.Customer.email == email_address)\ + .order_by(models.Customer.id.asc())\ + .all() + # fmt: on + + +def off_and_on_without_data(): + """All comments here are technically on the same prefix. + + The comments between will be formatted. This is a known limitation. + """ + # fmt: off + + # hey, that won't work + + # fmt: on + pass + + +def on_and_off_broken(): + """Another known limitation.""" + # fmt: on + # fmt: off + this=should.not_be.formatted() + and_=indeed . it is not formatted + because . the . handling . inside . generate_ignored_nodes() + now . considers . multiple . fmt . directives . within . one . prefix + # fmt: on + # fmt: off + # ...but comments still get reformatted even though they should not be + # fmt: on + + +def long_lines(): + if True: + typedargslist.extend( + gen_annotated_params( + ast_args.kwonlyargs, + ast_args.kw_defaults, + parameters, + implicit_default=True, + ) + ) + # fmt: off + a = ( + unnecessary_bracket() + ) + # fmt: on + _type_comment_re = re.compile( + r""" + ^ + [\t ]* + \#[ ]type:[ ]* + (?P + [^#\t\n]+? + ) + (? to match + # a trailing space which is why we need the silliness below + (? + (?:\#[^\n]*)? + \n? + ) + $ + """, + # fmt: off + re.MULTILINE|re.VERBOSE + # fmt: on + ) + + +def single_literal_yapf_disable(): + """Black does not support this.""" + BAZ = {(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)} # yapf: disable + + +cfg.rule( + "Default", + "address", + xxxx_xxxx=["xxx-xxxxxx-xxxxxxxxxx"], + xxxxxx="xx_xxxxx", + xxxxxxx="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + xxxxxxxxx_xxxx=True, + xxxxxxxx_xxxxxxxxxx=False, + xxxxxx_xxxxxx=2, + xxxxxx_xxxxx_xxxxxxxx=70, + xxxxxx_xxxxxx_xxxxx=True, + # fmt: off + xxxxxxx_xxxxxxxxxxxx={ + "xxxxxxxx": { + "xxxxxx": False, + "xxxxxxx": False, + "xxxx_xxxxxx": "xxxxx", + }, + "xxxxxxxx-xxxxx": { + "xxxxxx": False, + "xxxxxxx": True, + "xxxx_xxxxxx": "xxxxxx", + }, + }, + # fmt: on + xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, +) +# fmt: off +yield 'hello' +# No formatting to the end of the file +l=[1,2,3] +d={'a':1, + 'b':2} +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap new file mode 100644 index 0000000000..98e84a118f --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap @@ -0,0 +1,293 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip8.py +--- +## Input + +```py +# Make sure a leading comment is not removed. +def some_func( unformatted, args ): # fmt: skip + print("I am some_func") + return 0 + # Make sure this comment is not removed. + + +# Make sure a leading comment is not removed. +async def some_async_func( unformatted, args): # fmt: skip + print("I am some_async_func") + await asyncio.sleep(1) + + +# Make sure a leading comment is not removed. +class SomeClass( Unformatted, SuperClasses ): # fmt: skip + def some_method( self, unformatted, args ): # fmt: skip + print("I am some_method") + return 0 + + async def some_async_method( self, unformatted, args ): # fmt: skip + print("I am some_async_method") + await asyncio.sleep(1) + + +# Make sure a leading comment is not removed. +if unformatted_call( args ): # fmt: skip + print("First branch") + # Make sure this is not removed. +elif another_unformatted_call( args ): # fmt: skip + print("Second branch") +else : # fmt: skip + print("Last branch") + + +while some_condition( unformatted, args ): # fmt: skip + print("Do something") + + +for i in some_iter( unformatted, args ): # fmt: skip + print("Do something") + + +async def test_async_for(): + async for i in some_async_iter( unformatted, args ): # fmt: skip + print("Do something") + + +try : # fmt: skip + some_call() +except UnformattedError as ex: # fmt: skip + handle_exception() +finally : # fmt: skip + finally_call() + + +with give_me_context( unformatted, args ): # fmt: skip + print("Do something") + + +async def test_async_with(): + async with give_me_async_context( unformatted, args ): # fmt: skip + print("Do something") +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,62 +1,62 @@ + # Make sure a leading comment is not removed. +-def some_func( unformatted, args ): # fmt: skip ++def some_func(unformatted, args): # fmt: skip + print("I am some_func") + return 0 + # Make sure this comment is not removed. + + + # Make sure a leading comment is not removed. +-async def some_async_func( unformatted, args): # fmt: skip ++async def some_async_func(unformatted, args): # fmt: skip + print("I am some_async_func") + await asyncio.sleep(1) + + + # Make sure a leading comment is not removed. +-class SomeClass( Unformatted, SuperClasses ): # fmt: skip +- def some_method( self, unformatted, args ): # fmt: skip ++class SomeClass(Unformatted, SuperClasses): # fmt: skip ++ def some_method(self, unformatted, args): # fmt: skip + print("I am some_method") + return 0 + +- async def some_async_method( self, unformatted, args ): # fmt: skip ++ async def some_async_method(self, unformatted, args): # fmt: skip + print("I am some_async_method") + await asyncio.sleep(1) + + + # Make sure a leading comment is not removed. +-if unformatted_call( args ): # fmt: skip ++if unformatted_call(args): # fmt: skip + print("First branch") + # Make sure this is not removed. +-elif another_unformatted_call( args ): # fmt: skip ++elif another_unformatted_call(args): # fmt: skip + print("Second branch") +-else : # fmt: skip ++else: # fmt: skip + print("Last branch") + + +-while some_condition( unformatted, args ): # fmt: skip ++while some_condition(unformatted, args): # fmt: skip + print("Do something") + + +-for i in some_iter( unformatted, args ): # fmt: skip ++for i in some_iter(unformatted, args): # fmt: skip + print("Do something") + + + async def test_async_for(): +- async for i in some_async_iter( unformatted, args ): # fmt: skip ++ async for i in some_async_iter(unformatted, args): # fmt: skip + print("Do something") + + +-try : # fmt: skip ++try: # fmt: skip + some_call() +-except UnformattedError as ex: # fmt: skip ++except UnformattedError as ex: + handle_exception() +-finally : # fmt: skip ++finally: # fmt: skip + finally_call() + + +-with give_me_context( unformatted, args ): # fmt: skip ++with give_me_context(unformatted, args): # fmt: skip + print("Do something") + + + async def test_async_with(): +- async with give_me_async_context( unformatted, args ): # fmt: skip ++ async with give_me_async_context(unformatted, args): # fmt: skip + print("Do something") +``` + +## Ruff Output + +```py +# Make sure a leading comment is not removed. +def some_func(unformatted, args): # fmt: skip + print("I am some_func") + return 0 + # Make sure this comment is not removed. + + +# Make sure a leading comment is not removed. +async def some_async_func(unformatted, args): # fmt: skip + print("I am some_async_func") + await asyncio.sleep(1) + + +# Make sure a leading comment is not removed. +class SomeClass(Unformatted, SuperClasses): # fmt: skip + def some_method(self, unformatted, args): # fmt: skip + print("I am some_method") + return 0 + + async def some_async_method(self, unformatted, args): # fmt: skip + print("I am some_async_method") + await asyncio.sleep(1) + + +# Make sure a leading comment is not removed. +if unformatted_call(args): # fmt: skip + print("First branch") + # Make sure this is not removed. +elif another_unformatted_call(args): # fmt: skip + print("Second branch") +else: # fmt: skip + print("Last branch") + + +while some_condition(unformatted, args): # fmt: skip + print("Do something") + + +for i in some_iter(unformatted, args): # fmt: skip + print("Do something") + + +async def test_async_for(): + async for i in some_async_iter(unformatted, args): # fmt: skip + print("Do something") + + +try: # fmt: skip + some_call() +except UnformattedError as ex: + handle_exception() +finally: # fmt: skip + finally_call() + + +with give_me_context(unformatted, args): # fmt: skip + print("Do something") + + +async def test_async_with(): + async with give_me_async_context(unformatted, args): # fmt: skip + print("Do something") +``` + +## Black Output + +```py +# Make sure a leading comment is not removed. +def some_func( unformatted, args ): # fmt: skip + print("I am some_func") + return 0 + # Make sure this comment is not removed. + + +# Make sure a leading comment is not removed. +async def some_async_func( unformatted, args): # fmt: skip + print("I am some_async_func") + await asyncio.sleep(1) + + +# Make sure a leading comment is not removed. +class SomeClass( Unformatted, SuperClasses ): # fmt: skip + def some_method( self, unformatted, args ): # fmt: skip + print("I am some_method") + return 0 + + async def some_async_method( self, unformatted, args ): # fmt: skip + print("I am some_async_method") + await asyncio.sleep(1) + + +# Make sure a leading comment is not removed. +if unformatted_call( args ): # fmt: skip + print("First branch") + # Make sure this is not removed. +elif another_unformatted_call( args ): # fmt: skip + print("Second branch") +else : # fmt: skip + print("Last branch") + + +while some_condition( unformatted, args ): # fmt: skip + print("Do something") + + +for i in some_iter( unformatted, args ): # fmt: skip + print("Do something") + + +async def test_async_for(): + async for i in some_async_iter( unformatted, args ): # fmt: skip + print("Do something") + + +try : # fmt: skip + some_call() +except UnformattedError as ex: # fmt: skip + handle_exception() +finally : # fmt: skip + finally_call() + + +with give_me_context( unformatted, args ): # fmt: skip + print("Do something") + + +async def test_async_with(): + async with give_me_async_context( unformatted, args ): # fmt: skip + print("Do something") +``` + +