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")
+```
+
+