diff --git a/Cargo.lock b/Cargo.lock
index a56d657f14..4943a5476d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -863,9 +863,9 @@ dependencies = [
[[package]]
name = "fastrand"
-version = "1.8.0"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
dependencies = [
"instant",
]
@@ -2597,6 +2597,22 @@ dependencies = [
"rustc-hash",
]
+[[package]]
+name = "ruff_python_formatter"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "clap 4.1.4",
+ "insta",
+ "once_cell",
+ "ruff_formatter",
+ "ruff_text_size",
+ "rustc-hash",
+ "rustpython-common",
+ "rustpython-parser",
+ "test-case",
+]
+
[[package]]
name = "ruff_text_size"
version = "0.0.0"
@@ -3189,10 +3205,11 @@ dependencies = [
[[package]]
name = "thread_local"
-version = "1.1.4"
+version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
+checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
dependencies = [
+ "cfg-if",
"once_cell",
]
@@ -3311,9 +3328,9 @@ dependencies = [
[[package]]
name = "tokio-util"
-version = "0.7.6"
+version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc6a3b08b64e6dfad376fa2432c7b1f01522e37a623c3050bc95db2d3ff21583"
+checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
dependencies = [
"bytes",
"futures-core",
@@ -3972,9 +3989,9 @@ dependencies = [
[[package]]
name = "zune-inflate"
-version = "0.2.42"
+version = "0.2.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c473377c11c4a3ac6a2758f944cd336678e9c977aa0abf54f6450cf77e902d6d"
+checksum = "589245df6230839c305984dcc0a8385cc72af1fd223f360ffd5d65efa4216d40"
dependencies = [
"simd-adler32",
]
diff --git a/_typos.toml b/_typos.toml
index aef4905d73..0a98e4acc9 100644
--- a/_typos.toml
+++ b/_typos.toml
@@ -1,5 +1,5 @@
[files]
-extend-exclude = ["snapshots"]
+extend-exclude = ["snapshots", "black"]
[default.extend-words]
trivias = "trivias"
diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml
index adbf954c46..6d843c6302 100644
--- a/crates/ruff/Cargo.toml
+++ b/crates/ruff/Cargo.toml
@@ -21,7 +21,7 @@ bisection = { version = "0.1.0" }
bitflags = { version = "1.3.2" }
cfg-if = { version = "1.0.0" }
chrono = { version = "0.4.21", default-features = false, features = ["clock"] }
-clap = { version = "4.0.1", features = ["derive", "env"] }
+clap = { workspace = true, features = ["derive", "env"] }
colored = { version = "2.0.0" }
dirs = { version = "4.0.0" }
fern = { version = "0.6.1" }
diff --git a/crates/ruff/src/ast/helpers.rs b/crates/ruff/src/ast/helpers.rs
index 015327e1f9..04257cde9a 100644
--- a/crates/ruff/src/ast/helpers.rs
+++ b/crates/ruff/src/ast/helpers.rs
@@ -723,7 +723,7 @@ where
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
// Don't recurse.
}
- StmtKind::Return { value } => self.returns.push(value.as_ref().map(|expr| &**expr)),
+ StmtKind::Return { value } => self.returns.push(value.as_deref()),
_ => visitor::walk_stmt(self, stmt),
}
}
diff --git a/crates/ruff/src/checkers/ast.rs b/crates/ruff/src/checkers/ast.rs
index 711fa273fc..921cb4a5f6 100644
--- a/crates/ruff/src/checkers/ast.rs
+++ b/crates/ruff/src/checkers/ast.rs
@@ -1560,12 +1560,7 @@ where
pyflakes::rules::assert_tuple(self, stmt, test);
}
if self.settings.rules.enabled(&Rule::AssertFalse) {
- flake8_bugbear::rules::assert_false(
- self,
- stmt,
- test,
- msg.as_ref().map(|expr| &**expr),
- );
+ flake8_bugbear::rules::assert_false(self, stmt, test, msg.as_deref());
}
if self.settings.rules.enabled(&Rule::Assert) {
self.diagnostics
diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml
index 85a5e03f79..88f00cb535 100644
--- a/crates/ruff_cli/Cargo.toml
+++ b/crates/ruff_cli/Cargo.toml
@@ -33,7 +33,7 @@ bincode = { version = "1.3.3" }
bitflags = { version = "1.3.2" }
cachedir = { version = "0.3.0" }
chrono = { version = "0.4.21", default-features = false, features = ["clock"] }
-clap = { version = "4.0.1", features = ["derive", "env"] }
+clap = { workspace = true, features = ["derive", "env"] }
clap_complete_command = { version = "0.4.0" }
clearscreen = { version = "2.0.0" }
colored = { version = "2.0.0" }
diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml
new file mode 100644
index 0000000000..f421ac671c
--- /dev/null
+++ b/crates/ruff_python_formatter/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "ruff_python_formatter"
+version = "0.0.0"
+publish = false
+edition = "2021"
+
+[dependencies]
+anyhow = { workspace = true }
+clap = { workspace = true }
+once_cell = { workspace = true }
+ruff_formatter = { path = "../ruff_formatter" }
+ruff_text_size = { path = "../ruff_text_size" }
+rustc-hash = { workspace = true }
+rustpython-common = { workspace = true }
+rustpython-parser = { workspace = true }
+
+[dev-dependencies]
+insta = { version = "1.19.0", features = [] }
+test-case = { version = "2.2.2" }
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/attribute_access_on_number_literals.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/attribute_access_on_number_literals.py
new file mode 100644
index 0000000000..1507281ade
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/attribute_access_on_number_literals.py
@@ -0,0 +1,22 @@
+ax = 123456789 .bit_count()
+x = (123456).__abs__()
+x = .1.is_integer()
+x = 1. .imag
+x = 1E+1.imag
+x = 1E-1.real
+x = 123456789.123456789.hex()
+x = 123456789.123456789E123456789 .real
+x = 123456789E123456789 .conjugate()
+x = 123456789J.real
+x = 123456789.123456789J.__add__(0b1011.bit_length())
+x = 0XB1ACC.conjugate()
+x = 0B1011 .conjugate()
+x = 0O777 .real
+x = 0.000000006 .hex()
+x = -100.0000J
+
+if 10 .real:
+ ...
+
+y = 100[no]
+y = 100(no)
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/beginning_backslash.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/beginning_backslash.py
new file mode 100644
index 0000000000..3c0c70a94c
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/beginning_backslash.py
@@ -0,0 +1,7 @@
+\
+
+
+
+
+
+print("hello, world")
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/bracketmatch.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/bracketmatch.py
new file mode 100644
index 0000000000..a0f22e1923
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/bracketmatch.py
@@ -0,0 +1,6 @@
+for ((x in {}) or {})['a'] in x:
+ pass
+pem_spam = lambda l, spam = {
+ "x": 3
+}: not spam.get(l.strip())
+lambda x=lambda y={1: 3}: y['x':lambda y: {1: 2}]: x
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/class_blank_parentheses.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/class_blank_parentheses.py
new file mode 100644
index 0000000000..c82b02f298
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/class_blank_parentheses.py
@@ -0,0 +1,23 @@
+class SimpleClassWithBlankParentheses():
+ pass
+class ClassWithSpaceParentheses ( ):
+ first_test_data = 90
+ second_test_data = 100
+ def test_func(self):
+ return None
+class ClassWithEmptyFunc(object):
+
+ def func_with_blank_parentheses():
+ return 5
+
+
+def public_func_with_blank_parentheses():
+ return None
+def class_under_the_func_with_blank_parentheses():
+ class InsideFunc():
+ pass
+class NormalClass (
+):
+ def func_for_testing(self, first, second):
+ sum = first + second
+ return sum
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/class_methods_new_line.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/class_methods_new_line.py
new file mode 100644
index 0000000000..9749a4e81e
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/class_methods_new_line.py
@@ -0,0 +1,100 @@
+class ClassSimplest:
+ pass
+class ClassWithSingleField:
+ a = 1
+class ClassWithJustTheDocstring:
+ """Just a docstring."""
+class ClassWithInit:
+ def __init__(self):
+ pass
+class ClassWithTheDocstringAndInit:
+ """Just a docstring."""
+ def __init__(self):
+ pass
+class ClassWithInitAndVars:
+ cls_var = 100
+ def __init__(self):
+ pass
+class ClassWithInitAndVarsAndDocstring:
+ """Test class"""
+ cls_var = 100
+ def __init__(self):
+ pass
+class ClassWithDecoInit:
+ @deco
+ def __init__(self):
+ pass
+class ClassWithDecoInitAndVars:
+ cls_var = 100
+ @deco
+ def __init__(self):
+ pass
+class ClassWithDecoInitAndVarsAndDocstring:
+ """Test class"""
+ cls_var = 100
+ @deco
+ def __init__(self):
+ pass
+class ClassSimplestWithInner:
+ class Inner:
+ pass
+class ClassSimplestWithInnerWithDocstring:
+ class Inner:
+ """Just a docstring."""
+ def __init__(self):
+ pass
+class ClassWithSingleFieldWithInner:
+ a = 1
+ class Inner:
+ pass
+class ClassWithJustTheDocstringWithInner:
+ """Just a docstring."""
+ class Inner:
+ pass
+class ClassWithInitWithInner:
+ class Inner:
+ pass
+ def __init__(self):
+ pass
+class ClassWithInitAndVarsWithInner:
+ cls_var = 100
+ class Inner:
+ pass
+ def __init__(self):
+ pass
+class ClassWithInitAndVarsAndDocstringWithInner:
+ """Test class"""
+ cls_var = 100
+ class Inner:
+ pass
+ def __init__(self):
+ pass
+class ClassWithDecoInitWithInner:
+ class Inner:
+ pass
+ @deco
+ def __init__(self):
+ pass
+class ClassWithDecoInitAndVarsWithInner:
+ cls_var = 100
+ class Inner:
+ pass
+ @deco
+ def __init__(self):
+ pass
+class ClassWithDecoInitAndVarsAndDocstringWithInner:
+ """Test class"""
+ cls_var = 100
+ class Inner:
+ pass
+ @deco
+ def __init__(self):
+ pass
+class ClassWithDecoInitAndVarsAndDocstringWithInner2:
+ """Test class"""
+ class Inner:
+ pass
+ cls_var = 100
+ @deco
+ def __init__(self):
+ pass
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/collections.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/collections.py
new file mode 100644
index 0000000000..21dc78e184
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/collections.py
@@ -0,0 +1,71 @@
+import core, time, a
+
+from . import A, B, C
+
+# keeps existing trailing comma
+from foo import (
+ bar,
+)
+
+# also keeps existing structure
+from foo import (
+ baz,
+ qux,
+)
+
+# `as` works as well
+from foo import (
+ xyzzy as magic,
+)
+
+a = {1,2,3,}
+b = {
+1,2,
+ 3}
+c = {
+ 1,
+ 2,
+ 3,
+}
+x = 1,
+y = narf(),
+nested = {(1,2,3),(4,5,6),}
+nested_no_trailing_comma = {(1,2,3),(4,5,6)}
+nested_long_lines = ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "cccccccccccccccccccccccccccccccccccccccc", (1, 2, 3), "dddddddddddddddddddddddddddddddddddddddd"]
+{"oneple": (1,),}
+{"oneple": (1,)}
+['ls', 'lsoneple/%s' % (foo,)]
+x = {"oneple": (1,)}
+y = {"oneple": (1,),}
+assert False, ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa wraps %s" % bar)
+
+# looping over a 1-tuple should also not get wrapped
+for x in (1,):
+ pass
+for (x,) in (1,), (2,), (3,):
+ pass
+
+[1, 2, 3,]
+
+division_result_tuple = (6/2,)
+print("foo %r", (foo.bar,))
+
+if True:
+ IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = (
+ Config.IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING
+ | {pylons.controllers.WSGIController}
+ )
+
+if True:
+ ec2client.get_waiter('instance_stopped').wait(
+ InstanceIds=[instance.id],
+ WaiterConfig={
+ 'Delay': 5,
+ })
+ ec2client.get_waiter("instance_stopped").wait(
+ InstanceIds=[instance.id],
+ WaiterConfig={"Delay": 5,},
+ )
+ ec2client.get_waiter("instance_stopped").wait(
+ InstanceIds=[instance.id], WaiterConfig={"Delay": 5,},
+ )
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comment_after_escaped_newline.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comment_after_escaped_newline.py
new file mode 100644
index 0000000000..6700fcae4f
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comment_after_escaped_newline.py
@@ -0,0 +1,9 @@
+def bob(): \
+ # pylint: disable=W9016
+ pass
+
+
+def bobtwo(): \
+ \
+ # some comment here
+ pass
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py
new file mode 100644
index 0000000000..c34daaf6f0
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py
@@ -0,0 +1,96 @@
+#!/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/resources/test/fixtures/black/simple_cases/comments2.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments2.py
new file mode 100644
index 0000000000..d6a08c79f2
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments2.py
@@ -0,0 +1,165 @@
+from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import (
+ MyLovelyCompanyTeamProjectComponent # NOT DRY
+)
+from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import (
+ MyLovelyCompanyTeamProjectComponent as component # DRY
+)
+
+# Please keep __all__ alphabetized within each category.
+
+__all__ = [
+ # Super-special typing primitives.
+ 'Any',
+ 'Callable',
+ 'ClassVar',
+
+ # ABCs (from collections.abc).
+ 'AbstractSet', # collections.abc.Set.
+ 'ByteString',
+ 'Container',
+
+ # Concrete collection types.
+ 'Counter',
+ 'Deque',
+ 'Dict',
+ 'DefaultDict',
+ 'List',
+ 'Set',
+ 'FrozenSet',
+ 'NamedTuple', # Not really a type.
+ 'Generator',
+]
+
+not_shareables = [
+ # singletons
+ True,
+ False,
+ NotImplemented, ...,
+ # builtin types and objects
+ type,
+ object,
+ object(),
+ Exception(),
+ 42,
+ 100.0,
+ "spam",
+ # user-defined types and objects
+ Cheese,
+ Cheese("Wensleydale"),
+ SubBytes(b"spam"),
+]
+
+if 'PYTHON' in os.environ:
+ add_compiler(compiler_from_env())
+else:
+ # for compiler in compilers.values():
+ # add_compiler(compiler)
+ add_compiler(compilers[(7.0, 32)])
+ # add_compiler(compilers[(7.1, 64)])
+
+# Comment before function.
+def inline_comments_in_brackets_ruin_everything():
+ if typedargslist:
+ parameters.children = [
+ children[0], # (1
+ body,
+ children[-1] # )1
+ ]
+ parameters.children = [
+ children[0],
+ body,
+ children[-1], # type: ignore
+ ]
+ else:
+ parameters.children = [
+ parameters.children[0], # (2 what if this was actually long
+ body,
+ parameters.children[-1], # )2
+ ]
+ parameters.children = [parameters.what_if_this_was_actually_long.children[0], body, parameters.children[-1]] # type: ignore
+ if (self._proc is not None
+ # has the child process finished?
+ and self._returncode is None
+ # the child process has finished, but the
+ # transport hasn't been notified yet?
+ and self._proc.poll() is None):
+ pass
+ # no newline before or after
+ short = [
+ # one
+ 1,
+ # two
+ 2]
+
+ # no newline after
+ call(arg1, arg2, """
+short
+""", arg3=True)
+
+ ############################################################################
+
+ call2(
+ #short
+ arg1,
+ #but
+ arg2,
+ #multiline
+ """
+short
+""",
+ # yup
+ arg3=True)
+ lcomp = [
+ element # yup
+ for element in collection # yup
+ if element is not None # right
+ ]
+ lcomp2 = [
+ # hello
+ element
+ # yup
+ for element in collection
+ # right
+ if element is not None
+ ]
+ 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
+ ]
+ while True:
+ if False:
+ continue
+
+ # and round and round we go
+ # and round and round we go
+
+ # let's return
+ return Node(
+ syms.simple_stmt,
+ [
+ Node(statement, result),
+ Leaf(token.NEWLINE, '\n') # FIXME: \r\n?
+ ],
+ )
+
+CONFIG_FILES = [CONFIG_FILE, ] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final
+
+class Test:
+ def _init_host(self, parsed) -> None:
+ if (parsed.hostname is None or # type: ignore
+ not parsed.hostname.strip()):
+ pass
+
+#######################
+### SECTION COMMENT ###
+#######################
+
+
+instruction()#comment with bad spacing
+
+# END COMMENTS
+# MORE END COMMENTS
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments3.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments3.py
new file mode 100644
index 0000000000..1bab9733b1
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments3.py
@@ -0,0 +1,47 @@
+# 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/resources/test/fixtures/black/simple_cases/comments4.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments4.py
new file mode 100644
index 0000000000..2147d41c9d
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments4.py
@@ -0,0 +1,94 @@
+from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import (
+ MyLovelyCompanyTeamProjectComponent, # NOT DRY
+)
+from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import (
+ MyLovelyCompanyTeamProjectComponent as component, # DRY
+)
+
+
+class C:
+ @pytest.mark.parametrize(
+ ("post_data", "message"),
+ [
+ # metadata_version errors.
+ (
+ {},
+ "None is an invalid value for Metadata-Version. Error: This field is"
+ " required. see"
+ " https://packaging.python.org/specifications/core-metadata",
+ ),
+ (
+ {"metadata_version": "-1"},
+ "'-1' is an invalid value for Metadata-Version. Error: Unknown Metadata"
+ " Version see"
+ " https://packaging.python.org/specifications/core-metadata",
+ ),
+ # name errors.
+ (
+ {"metadata_version": "1.2"},
+ "'' is an invalid value for Name. Error: This field is required. see"
+ " https://packaging.python.org/specifications/core-metadata",
+ ),
+ (
+ {"metadata_version": "1.2", "name": "foo-"},
+ "'foo-' is an invalid value for Name. Error: Must start and end with a"
+ " letter or numeral and contain only ascii numeric and '.', '_' and"
+ " '-'. see https://packaging.python.org/specifications/core-metadata",
+ ),
+ # version errors.
+ (
+ {"metadata_version": "1.2", "name": "example"},
+ "'' is an invalid value for Version. Error: This field is required. see"
+ " https://packaging.python.org/specifications/core-metadata",
+ ),
+ (
+ {"metadata_version": "1.2", "name": "example", "version": "dog"},
+ "'dog' is an invalid value for Version. Error: Must start and end with"
+ " a letter or numeral and contain only ascii numeric and '.', '_' and"
+ " '-'. see https://packaging.python.org/specifications/core-metadata",
+ ),
+ ],
+ )
+ def test_fails_invalid_post_data(
+ self, pyramid_config, db_request, post_data, message
+ ):
+ pyramid_config.testing_securitypolicy(userid=1)
+ db_request.POST = MultiDict(post_data)
+
+
+def foo(list_a, list_b):
+ results = (
+ User.query.filter(User.foo == "bar")
+ .filter( # Because foo.
+ db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
+ )
+ .filter(User.xyz.is_(None))
+ # Another comment about the filtering on is_quux goes here.
+ .filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True)))
+ .order_by(User.created_at.desc())
+ .with_for_update(key_share=True)
+ .all()
+ )
+ return results
+
+
+def foo2(list_a, list_b):
+ # Standalone comment reasonably placed.
+ return (
+ User.query.filter(User.foo == "bar")
+ .filter(
+ db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
+ )
+ .filter(User.xyz.is_(None))
+ )
+
+
+def foo3(list_a, list_b):
+ return (
+ # Standlone comment but weirdly placed.
+ User.query.filter(User.foo == "bar")
+ .filter(
+ db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
+ )
+ .filter(User.xyz.is_(None))
+ )
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments5.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments5.py
new file mode 100644
index 0000000000..c8c38813d5
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments5.py
@@ -0,0 +1,71 @@
+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/resources/test/fixtures/black/simple_cases/comments6.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments6.py
new file mode 100644
index 0000000000..735c6aa6d7
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments6.py
@@ -0,0 +1,118 @@
+from typing import Any, Tuple
+
+
+def f(
+ a, # type: int
+):
+ pass
+
+
+# test type comments
+def f(a, b, c, d, e, f, g, h, i):
+ # type: (int, int, int, int, int, int, int, int, int) -> None
+ pass
+
+
+def f(
+ a, # type: int
+ b, # type: int
+ c, # type: int
+ d, # type: int
+ e, # type: int
+ f, # type: int
+ g, # type: int
+ h, # type: int
+ i, # type: int
+):
+ # type: (...) -> None
+ pass
+
+
+def f(
+ arg, # type: int
+ *args, # type: *Any
+ default=False, # type: bool
+ **kwargs, # type: **Any
+):
+ # type: (...) -> None
+ pass
+
+
+def f(
+ a, # type: int
+ b, # type: int
+ c, # type: int
+ d, # type: int
+):
+ # type: (...) -> None
+
+ element = 0 # type: int
+ another_element = 1 # type: float
+ another_element_with_long_name = 2 # type: int
+ another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style = (
+ 3
+ ) # type: int
+ an_element_with_a_long_value = calls() or more_calls() and more() # type: bool
+
+ tup = (
+ another_element,
+ another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style,
+ ) # type: Tuple[int, int]
+
+ a = (
+ element
+ + another_element
+ + another_element_with_long_name
+ + element
+ + another_element
+ + another_element_with_long_name
+ ) # type: int
+
+
+def f(
+ x, # not a type comment
+ y, # type: int
+):
+ # type: (...) -> None
+ pass
+
+
+def f(
+ x, # not a type comment
+): # type: (int) -> None
+ pass
+
+
+def func(
+ a=some_list[0], # type: int
+): # type: () -> int
+ c = call(
+ 0.0123,
+ 0.0456,
+ 0.0789,
+ 0.0123,
+ 0.0456,
+ 0.0789,
+ 0.0123,
+ 0.0456,
+ 0.0789,
+ a[-1], # type: ignore
+ )
+
+ c = call(
+ "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa" # type: ignore
+ )
+
+
+result = ( # aaa
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+)
+
+AAAAAAAAAAAAA = [AAAAAAAAAAAAA] + SHARED_AAAAAAAAAAAAA + USER_AAAAAAAAAAAAA + AAAAAAAAAAAAA # type: ignore
+
+call_to_some_function_asdf(
+ foo,
+ [AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, BBBBBBBBBBBB], # type: ignore
+)
+
+aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type]
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments_non_breaking_space.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments_non_breaking_space.py
new file mode 100644
index 0000000000..d1d42f0259
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments_non_breaking_space.py
@@ -0,0 +1,19 @@
+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/resources/test/fixtures/black/simple_cases/composition.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/composition.py
new file mode 100644
index 0000000000..e429f15e66
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/composition.py
@@ -0,0 +1,181 @@
+class C:
+ def test(self) -> None:
+ with patch("black.out", print):
+ self.assertEqual(
+ unstyle(str(report)), "1 file reformatted, 1 file failed to reformat."
+ )
+ self.assertEqual(
+ unstyle(str(report)),
+ "1 file reformatted, 1 file left unchanged, 1 file failed to reformat.",
+ )
+ self.assertEqual(
+ unstyle(str(report)),
+ "2 files reformatted, 1 file left unchanged, 1 file failed to"
+ " reformat.",
+ )
+ self.assertEqual(
+ unstyle(str(report)),
+ "2 files reformatted, 2 files left unchanged, 2 files failed to"
+ " reformat.",
+ )
+ for i in (a,):
+ if (
+ # Rule 1
+ i % 2 == 0
+ # Rule 2
+ and i % 3 == 0
+ ):
+ while (
+ # Just a comment
+ call()
+ # Another
+ ):
+ print(i)
+ xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy(
+ push_manager=context.request.resource_manager,
+ max_items_to_push=num_items,
+ batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE,
+ ).push(
+ # Only send the first n items.
+ items=items[:num_items]
+ )
+ return (
+ 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s'
+ % (test.name, test.filename, lineno, lname, err)
+ )
+
+ def omitting_trailers(self) -> None:
+ get_collection(
+ hey_this_is_a_very_long_call, it_has_funny_attributes, really=True
+ )[OneLevelIndex]
+ get_collection(
+ hey_this_is_a_very_long_call, it_has_funny_attributes, really=True
+ )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex]
+ d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][
+ 22
+ ]
+ assignment = (
+ some.rather.elaborate.rule() and another.rule.ending_with.index[123]
+ )
+
+ def easy_asserts(self) -> None:
+ assert {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9,
+ } == expected, "Not what we expected"
+
+ assert expected == {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9,
+ }, "Not what we expected"
+
+ assert expected == {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9,
+ }
+
+ def tricky_asserts(self) -> None:
+ assert {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9,
+ } == expected(
+ value, is_going_to_be="too long to fit in a single line", srsly=True
+ ), "Not what we expected"
+
+ assert {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9,
+ } == expected, (
+ "Not what we expected and the message is too long to fit in one line"
+ )
+
+ assert expected(
+ value, is_going_to_be="too long to fit in a single line", srsly=True
+ ) == {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9,
+ }, "Not what we expected"
+
+ assert expected == {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9,
+ }, (
+ "Not what we expected and the message is too long to fit in one line"
+ " because it's too long"
+ )
+
+ dis_c_instance_method = """\
+ %3d 0 LOAD_FAST 1 (x)
+ 2 LOAD_CONST 1 (1)
+ 4 COMPARE_OP 2 (==)
+ 6 LOAD_FAST 0 (self)
+ 8 STORE_ATTR 0 (x)
+ 10 LOAD_CONST 0 (None)
+ 12 RETURN_VALUE
+ """ % (
+ _C.__init__.__code__.co_firstlineno + 1,
+ )
+
+ assert (
+ expectedexpectedexpectedexpectedexpectedexpectedexpectedexpectedexpect
+ == {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9,
+ }
+ )
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/composition_no_trailing_comma.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/composition_no_trailing_comma.py
new file mode 100644
index 0000000000..7c77d1f593
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/composition_no_trailing_comma.py
@@ -0,0 +1,181 @@
+class C:
+ def test(self) -> None:
+ with patch("black.out", print):
+ self.assertEqual(
+ unstyle(str(report)), "1 file reformatted, 1 file failed to reformat."
+ )
+ self.assertEqual(
+ unstyle(str(report)),
+ "1 file reformatted, 1 file left unchanged, 1 file failed to reformat.",
+ )
+ self.assertEqual(
+ unstyle(str(report)),
+ "2 files reformatted, 1 file left unchanged, 1 file failed to"
+ " reformat.",
+ )
+ self.assertEqual(
+ unstyle(str(report)),
+ "2 files reformatted, 2 files left unchanged, 2 files failed to"
+ " reformat.",
+ )
+ for i in (a,):
+ if (
+ # Rule 1
+ i % 2 == 0
+ # Rule 2
+ and i % 3 == 0
+ ):
+ while (
+ # Just a comment
+ call()
+ # Another
+ ):
+ print(i)
+ xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy(
+ push_manager=context.request.resource_manager,
+ max_items_to_push=num_items,
+ batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE
+ ).push(
+ # Only send the first n items.
+ items=items[:num_items]
+ )
+ return (
+ 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s'
+ % (test.name, test.filename, lineno, lname, err)
+ )
+
+ def omitting_trailers(self) -> None:
+ get_collection(
+ hey_this_is_a_very_long_call, it_has_funny_attributes, really=True
+ )[OneLevelIndex]
+ get_collection(
+ hey_this_is_a_very_long_call, it_has_funny_attributes, really=True
+ )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex]
+ d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][
+ 22
+ ]
+ assignment = (
+ some.rather.elaborate.rule() and another.rule.ending_with.index[123]
+ )
+
+ def easy_asserts(self) -> None:
+ assert {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9
+ } == expected, "Not what we expected"
+
+ assert expected == {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9
+ }, "Not what we expected"
+
+ assert expected == {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9
+ }
+
+ def tricky_asserts(self) -> None:
+ assert {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9
+ } == expected(
+ value, is_going_to_be="too long to fit in a single line", srsly=True
+ ), "Not what we expected"
+
+ assert {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9
+ } == expected, (
+ "Not what we expected and the message is too long to fit in one line"
+ )
+
+ assert expected(
+ value, is_going_to_be="too long to fit in a single line", srsly=True
+ ) == {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9
+ }, "Not what we expected"
+
+ assert expected == {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9
+ }, (
+ "Not what we expected and the message is too long to fit in one line"
+ " because it's too long"
+ )
+
+ dis_c_instance_method = """\
+ %3d 0 LOAD_FAST 1 (x)
+ 2 LOAD_CONST 1 (1)
+ 4 COMPARE_OP 2 (==)
+ 6 LOAD_FAST 0 (self)
+ 8 STORE_ATTR 0 (x)
+ 10 LOAD_CONST 0 (None)
+ 12 RETURN_VALUE
+ """ % (
+ _C.__init__.__code__.co_firstlineno + 1,
+ )
+
+ assert (
+ expectedexpectedexpectedexpectedexpectedexpectedexpectedexpectedexpect
+ == {
+ key1: value1,
+ key2: value2,
+ key3: value3,
+ key4: value4,
+ key5: value5,
+ key6: value6,
+ key7: value7,
+ key8: value8,
+ key9: value9
+ }
+ )
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/docstring.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/docstring.py
new file mode 100644
index 0000000000..e1725f1f4f
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/docstring.py
@@ -0,0 +1,221 @@
+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/resources/test/fixtures/black/simple_cases/docstring_no_extra_empty_line_before_eof.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/docstring_no_extra_empty_line_before_eof.py
new file mode 100644
index 0000000000..6fea860adf
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/docstring_no_extra_empty_line_before_eof.py
@@ -0,0 +1,4 @@
+# Make sure when the file ends with class's docstring,
+# It doesn't add extra blank lines.
+class ClassWithDocstring:
+ """A docstring."""
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/empty_lines.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/empty_lines.py
new file mode 100644
index 0000000000..2a9416bb16
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/empty_lines.py
@@ -0,0 +1,92 @@
+"""Docstring."""
+
+
+# leading comment
+def f():
+ NO = ''
+ SPACE = ' '
+ DOUBLESPACE = ' '
+
+ t = leaf.type
+ p = leaf.parent # trailing comment
+ v = leaf.value
+
+ if t in ALWAYS_NO_SPACE:
+ pass
+ if t == token.COMMENT: # another trailing comment
+ return DOUBLESPACE
+
+
+ assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}"
+
+
+ prev = leaf.prev_sibling
+ if not prev:
+ prevp = preceding_leaf(p)
+ if not prevp or prevp.type in OPENING_BRACKETS:
+
+
+ return NO
+
+
+ if prevp.type == token.EQUAL:
+ if prevp.parent and prevp.parent.type in {
+ syms.typedargslist,
+ syms.varargslist,
+ syms.parameters,
+ syms.arglist,
+ syms.argument,
+ }:
+ return NO
+
+ elif prevp.type == token.DOUBLESTAR:
+ if prevp.parent and prevp.parent.type in {
+ syms.typedargslist,
+ syms.varargslist,
+ syms.parameters,
+ syms.arglist,
+ syms.dictsetmaker,
+ }:
+ return NO
+
+###############################################################################
+# SECTION BECAUSE SECTIONS
+###############################################################################
+
+def g():
+ NO = ''
+ SPACE = ' '
+ DOUBLESPACE = ' '
+
+ t = leaf.type
+ p = leaf.parent
+ v = leaf.value
+
+ # Comment because comments
+
+ if t in ALWAYS_NO_SPACE:
+ pass
+ if t == token.COMMENT:
+ return DOUBLESPACE
+
+ # Another comment because more comments
+ assert p is not None, f'INTERNAL ERROR: hand-made leaf without parent: {leaf!r}'
+
+ prev = leaf.prev_sibling
+ if not prev:
+ prevp = preceding_leaf(p)
+
+ if not prevp or prevp.type in OPENING_BRACKETS:
+ # Start of the line or a bracketed expression.
+ # More than one line for the comment.
+ return NO
+
+ if prevp.type == token.EQUAL:
+ if prevp.parent and prevp.parent.type in {
+ syms.typedargslist,
+ syms.varargslist,
+ syms.parameters,
+ syms.arglist,
+ syms.argument,
+ }:
+ return NO
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py
new file mode 100644
index 0000000000..e660d7969d
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py
@@ -0,0 +1,254 @@
+...
+'some_string'
+b'\\xa3'
+Name
+None
+True
+False
+1
+1.0
+1j
+True or False
+True or False or None
+True and False
+True and False and None
+(Name1 and Name2) or Name3
+Name1 and Name2 or Name3
+Name1 or (Name2 and Name3)
+Name1 or Name2 and Name3
+(Name1 and Name2) or (Name3 and Name4)
+Name1 and Name2 or Name3 and Name4
+Name1 or (Name2 and Name3) or Name4
+Name1 or Name2 and Name3 or Name4
+v1 << 2
+1 >> v2
+1 % finished
+1 + v2 - v3 * 4 ^ 5 ** v6 / 7 // 8
+((1 + v2) - (v3 * 4)) ^ (((5 ** v6) / 7) // 8)
+not great
+~great
++value
+-1
+~int and not v1 ^ 123 + v2 | True
+(~int) and (not ((v1 ^ (123 + v2)) | True))
++really ** -confusing ** ~operator ** -precedence
+flags & ~ select.EPOLLIN and waiters.write_task is not None
+lambda arg: None
+lambda a=True: a
+lambda a, b, c=True: a
+lambda a, b, c=True, *, d=(1 << v2), e='str': a
+lambda a, b, c=True, *vararg, d=(v1 << 2), e='str', **kwargs: a + b
+manylambdas = lambda x=lambda y=lambda z=1: z: y(): x()
+foo = (lambda port_id, ignore_missing: {"port1": port1_resource, "port2": port2_resource}[port_id])
+1 if True else 2
+str or None if True else str or bytes or None
+(str or None) if True else (str or bytes or None)
+str or None if (1 if True else 2) else str or bytes or None
+(str or None) if (1 if True else 2) else (str or bytes or None)
+((super_long_variable_name or None) if (1 if super_long_test_name else 2) else (str or bytes or None))
+{'2.7': dead, '3.7': (long_live or die_hard)}
+{'2.7': dead, '3.7': (long_live or die_hard), **{'3.6': verygood}}
+{**a, **b, **c}
+{'2.7', '3.6', '3.7', '3.8', '3.9', ('4.0' if gilectomy else '3.10')}
+({'a': 'b'}, (True or False), (+value), 'string', b'bytes') or None
+()
+(1,)
+(1, 2)
+(1, 2, 3)
+[]
+[1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)]
+[1, 2, 3,]
+[*a]
+[*range(10)]
+[*a, 4, 5,]
+[4, *a, 5,]
+[this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, another, *more]
+{i for i in (1, 2, 3)}
+{(i ** 2) for i in (1, 2, 3)}
+{(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))}
+{((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)}
+[i for i in (1, 2, 3)]
+[(i ** 2) for i in (1, 2, 3)]
+[(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))]
+[((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)]
+{i: 0 for i in (1, 2, 3)}
+{i: j for i, j in ((1, 'a'), (2, 'b'), (3, 'c'))}
+{a: b * 2 for a, b in dictionary.items()}
+{a: b * -2 for a, b in dictionary.items()}
+{k: v for k, v in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension}
+Python3 > Python2 > COBOL
+Life is Life
+call()
+call(arg)
+call(kwarg='hey')
+call(arg, kwarg='hey')
+call(arg, another, kwarg='hey', **kwargs)
+call(this_is_a_very_long_variable_which_will_force_a_delimiter_split, arg, another, kwarg='hey', **kwargs) # note: no trailing comma pre-3.6
+call(*gidgets[:2])
+call(a, *gidgets[:2])
+call(**self.screen_kwargs)
+call(b, **self.screen_kwargs)
+lukasz.langa.pl
+call.me(maybe)
+1 .real
+1.0 .real
+....__class__
+list[str]
+dict[str, int]
+tuple[str, ...]
+tuple[
+ str, int, float, dict[str, int]
+]
+tuple[str, int, float, dict[str, int],]
+very_long_variable_name_filters: t.List[
+ t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]],
+]
+xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore
+ sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)
+)
+xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore
+ sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)
+)
+xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[
+ ..., List[SomeClass]
+] = classmethod(sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)) # type: ignore
+slice[0]
+slice[0:1]
+slice[0:1:2]
+slice[:]
+slice[:-1]
+slice[1:]
+slice[::-1]
+slice[d :: d + 1]
+slice[:c, c - 1]
+numpy[:, 0:1]
+numpy[:, :-1]
+numpy[0, :]
+numpy[:, i]
+numpy[0, :2]
+numpy[:N, 0]
+numpy[:2, :4]
+numpy[2:4, 1:5]
+numpy[4:, 2:]
+numpy[:, (0, 1, 2, 5)]
+numpy[0, [0]]
+numpy[:, [i]]
+numpy[1 : c + 1, c]
+numpy[-(c + 1) :, d]
+numpy[:, l[-2]]
+numpy[:, ::-1]
+numpy[np.newaxis, :]
+(str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None)
+{'2.7': dead, '3.7': long_live or die_hard}
+{'2.7', '3.6', '3.7', '3.8', '3.9', '4.0' if gilectomy else '3.10'}
+[1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C]
+(SomeName)
+SomeName
+(Good, Bad, Ugly)
+(i for i in (1, 2, 3))
+((i ** 2) for i in (1, 2, 3))
+((i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c')))
+(((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3))
+(*starred,)
+{"id": "1","type": "type","started_at": now(),"ended_at": now() + timedelta(days=10),"priority": 1,"import_session_id": 1,**kwargs}
+a = (1,)
+b = 1,
+c = 1
+d = (1,) + a + (2,)
+e = (1,).count(1)
+f = 1, *range(10)
+g = 1, *"ten"
+what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set(vars_to_remove)
+what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set(vars_to_remove)
+result = session.query(models.Customer.id).filter(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()
+Ø = set()
+authors.łukasz.say_thanks()
+mapping = {
+ A: 0.25 * (10.0 / 12),
+ B: 0.1 * (10.0 / 12),
+ C: 0.1 * (10.0 / 12),
+ D: 0.1 * (10.0 / 12),
+}
+
+def gen():
+ yield from outside_of_generator
+ a = (yield)
+ b = ((yield))
+ c = (((yield)))
+
+async def f():
+ await some.complicated[0].call(with_args=(True or (1 is not 1)))
+print(* [] or [1])
+print(**{1: 3} if False else {x: x for x in range(3)})
+print(* lambda x: x)
+assert(not Test),("Short message")
+assert this is ComplexTest and not requirements.fit_in_a_single_line(force=False), "Short message"
+assert(((parens is TooMany)))
+for x, in (1,), (2,), (3,): ...
+for y in (): ...
+for z in (i for i in (1, 2, 3)): ...
+for i in (call()): ...
+for j in (1 + (2 + 3)): ...
+while(this and that): ...
+for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'):
+ pass
+a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz
+a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp not in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz
+a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp is qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz
+a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz
+if (
+ threading.current_thread() != threading.main_thread() and
+ threading.current_thread() != threading.main_thread() or
+ signal.getsignal(signal.SIGINT) != signal.default_int_handler
+):
+ return True
+if (
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+):
+ return True
+if (
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa &
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+):
+ return True
+if (
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+):
+ return True
+if (
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+):
+ return True
+if (
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa *
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+):
+ return True
+if (
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa /
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+):
+ return True
+if (
+ ~ aaaa.a + aaaa.b - aaaa.c * aaaa.d / aaaa.e | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l ** aaaa.m // aaaa.n
+):
+ return True
+if (
+ ~ aaaaaaaa.a + aaaaaaaa.b - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e | aaaaaaaa.f & aaaaaaaa.g % aaaaaaaa.h ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l ** aaaaaaaa.m // aaaaaaaa.n
+):
+ return True
+if (
+ ~ aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n
+):
+ return True
+aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa - aaaaaaaaaaaaaaaa * (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) / (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa)
+aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa >> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa << aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+bbbb >> bbbb * bbbb
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ^bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+last_call()
+# standalone comment at ENDMARKER
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py
new file mode 100644
index 0000000000..0a46e0feb8
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py
@@ -0,0 +1,186 @@
+#!/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/resources/test/fixtures/black/simple_cases/fmtonoff2.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff2.py
new file mode 100644
index 0000000000..e8657c749b
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff2.py
@@ -0,0 +1,40 @@
+import pytest
+
+TmSt = 1
+TmEx = 2
+
+# fmt: off
+
+# Test data:
+# Position, Volume, State, TmSt/TmEx/None, [call, [arg1...]]
+
+@pytest.mark.parametrize('test', [
+
+ # Test don't manage the volume
+ [
+ ('stuff', 'in')
+ ],
+])
+def test_fader(test):
+ pass
+
+def check_fader(test):
+
+ pass
+
+def verify_fader(test):
+ # misaligned comment
+ pass
+
+def verify_fader(test):
+ """Hey, ho."""
+ assert test.passed()
+
+def test_calculate_fades():
+ calcs = [
+ # one is zero/none
+ (0, 4, 0, 0, 10, 0, 0, 6, 10),
+ (None, 4, 0, 0, 10, 0, 0, 6, 10),
+ ]
+
+# fmt: on
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff3.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff3.py
new file mode 100644
index 0000000000..a7a196669a
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff3.py
@@ -0,0 +1,17 @@
+# fmt: off
+x = [
+ 1, 2,
+ 3, 4,
+]
+# fmt: on
+
+# fmt: off
+x = [
+ 1, 2,
+ 3, 4,
+]
+# fmt: on
+
+x = [
+ 1, 2, 3, 4
+]
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff4.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff4.py
new file mode 100644
index 0000000000..70dfb17324
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff4.py
@@ -0,0 +1,13 @@
+# fmt: off
+@test([
+ 1, 2,
+ 3, 4,
+])
+# fmt: on
+def f(): pass
+
+@test([
+ 1, 2,
+ 3, 4,
+])
+def f(): pass
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff5.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff5.py
new file mode 100644
index 0000000000..81e9be73c3
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff5.py
@@ -0,0 +1,84 @@
+# Regression test for https://github.com/psf/black/issues/3129.
+setup(
+ entry_points={
+ # fmt: off
+ "console_scripts": [
+ "foo-bar"
+ "=foo.bar.:main",
+ # fmt: on
+ ] # Includes an formatted indentation.
+ },
+)
+
+
+# Regression test for https://github.com/psf/black/issues/2015.
+run(
+ # fmt: off
+ [
+ "ls",
+ "-la",
+ ]
+ # fmt: on
+ + path,
+ check=True,
+)
+
+
+# Regression test for https://github.com/psf/black/issues/3026.
+def test_func():
+ # yapf: disable
+ if unformatted( args ):
+ return True
+ # yapf: enable
+ elif b:
+ return True
+
+ return False
+
+
+# Regression test for https://github.com/psf/black/issues/2567.
+if True:
+ # fmt: off
+ for _ in range( 1 ):
+ # fmt: on
+ print ( "This won't be formatted" )
+ print ( "This won't be formatted either" )
+else:
+ print ( "This will be formatted" )
+
+
+# Regression test for https://github.com/psf/black/issues/3184.
+class A:
+ async def call(param):
+ if param:
+ # fmt: off
+ if param[0:4] in (
+ "ABCD", "EFGH"
+ ) :
+ # fmt: on
+ print ( "This won't be formatted" )
+
+ elif param[0:4] in ("ZZZZ",):
+ print ( "This won't be formatted either" )
+
+ print ( "This will be formatted" )
+
+
+# Regression test for https://github.com/psf/black/issues/2985.
+class Named(t.Protocol):
+ # fmt: off
+ @property
+ def this_wont_be_formatted ( self ) -> str: ...
+
+class Factory(t.Protocol):
+ def this_will_be_formatted ( self, **kwargs ) -> Named: ...
+ # fmt: on
+
+
+# Regression test for https://github.com/psf/black/issues/3436.
+if x:
+ return x
+# fmt: off
+elif unformatted:
+# fmt: on
+ will_be_formatted ()
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip.py
new file mode 100644
index 0000000000..1d5836fc03
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip.py
@@ -0,0 +1,3 @@
+a, b = 1, 2
+c = 6 # fmt: skip
+d = 5
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip2.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip2.py
new file mode 100644
index 0000000000..b4a792c16d
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip2.py
@@ -0,0 +1,3 @@
+l1 = ["This list should be broken up", "into multiple lines", "because it is way too long"]
+l2 = ["But this list shouldn't", "even though it also has", "way too many characters in it"] # fmt: skip
+l3 = ["I have", "trailing comma", "so I should be braked",]
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip3.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip3.py
new file mode 100644
index 0000000000..bc0eca4ddb
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip3.py
@@ -0,0 +1,7 @@
+a = 3
+# fmt: off
+b, c = 1, 2
+d = 6 # fmt: skip
+e = 5
+# fmt: on
+f = ["This is a very long line that should be formatted into a clearer line ", "by rearranging."]
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip4.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip4.py
new file mode 100644
index 0000000000..258d40b363
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip4.py
@@ -0,0 +1,3 @@
+a = 2
+# fmt: skip
+l = [1, 2, 3,]
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip5.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip5.py
new file mode 100644
index 0000000000..873f0d6942
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip5.py
@@ -0,0 +1,9 @@
+a, b, c = 3, 4, 5
+if (
+ a == 3
+ and b != 9 # fmt: skip
+ and c is not None
+):
+ print("I'm good!")
+else:
+ print("I'm bad")
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip6.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip6.py
new file mode 100644
index 0000000000..cf829dbdb1
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip6.py
@@ -0,0 +1,5 @@
+class A:
+ def f(self):
+ for line in range(10):
+ if True:
+ pass # fmt: skip
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip7.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip7.py
new file mode 100644
index 0000000000..5d7d9a4f31
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip7.py
@@ -0,0 +1,4 @@
+a = "this is some code"
+b = 5 #fmt:skip
+c = 9 #fmt: skip
+d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" #fmt:skip
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip8.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip8.py
new file mode 100644
index 0000000000..38e9c2a9f4
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip8.py
@@ -0,0 +1,62 @@
+# 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")
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py
new file mode 100644
index 0000000000..b778ec2879
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py
@@ -0,0 +1,9 @@
+f"f-string without formatted values is just a string"
+f"{{NOT a formatted value}}"
+f"{{NOT 'a' \"formatted\" \"value\"}}"
+f"some f-string with {a} {few():.2f} {formatted.values!r}"
+f'some f-string with {a} {few(""):.2f} {formatted.values!r}'
+f"{f'''{'nested'} inner'''} outer"
+f"\"{f'{nested} inner'}\" outer"
+f"space between opening braces: { {a for a in (1, 2, 3)}}"
+f'Hello \'{tricky + "example"}\''
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py
new file mode 100644
index 0000000000..1195501740
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+import asyncio
+import sys
+
+from third_party import X, Y, Z
+
+from library import some_connection, \
+ some_decorator
+f'trigger 3.6 mode'
+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]
+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(10000, 200000)))
+ 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)):
+ assert fut is self._read_fut, (fut, self._read_fut)
+
+def example(session):
+ 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()
+def long_lines():
+ if True:
+ typedargslist.extend(
+ gen_annotated_params(ast_args.kwonlyargs, ast_args.kw_defaults, parameters, implicit_default=True)
+ )
+ typedargslist.extend(
+ gen_annotated_params(
+ ast_args.kwonlyargs, ast_args.kw_defaults, parameters, implicit_default=True,
+ # trailing standalone comment
+ )
+ )
+ _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?
+ )
+ $
+ """, re.MULTILINE | re.VERBOSE
+ )
+def trailing_comma():
+ mapping = {
+ A: 0.25 * (10.0 / 12),
+ B: 0.1 * (10.0 / 12),
+ C: 0.1 * (10.0 / 12),
+ D: 0.1 * (10.0 / 12),
+}
+def f(
+ a,
+ **kwargs,
+) -> A:
+ return (
+ yield from A(
+ very_long_argument_name1=very_long_value_for_the_argument,
+ very_long_argument_name2=very_long_value_for_the_argument,
+ **kwargs,
+ )
+ )
+def __await__(): return (yield)
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function2.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function2.py
new file mode 100644
index 0000000000..b1fa9585c9
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function2.py
@@ -0,0 +1,53 @@
+def f(
+ a,
+ **kwargs,
+) -> A:
+ with cache_dir():
+ if something:
+ result = (
+ CliRunner().invoke(black.main, [str(src1), str(src2), "--diff", "--check"])
+ )
+ limited.append(-limited.pop()) # negate top
+ return A(
+ very_long_argument_name1=very_long_value_for_the_argument,
+ very_long_argument_name2=-very.long.value.for_the_argument,
+ **kwargs,
+ )
+def g():
+ "Docstring."
+ def inner():
+ pass
+ print("Inner defs should breathe a little.")
+def h():
+ def inner():
+ pass
+ print("Inner defs should breathe a little.")
+
+
+if os.name == "posix":
+ import termios
+ def i_should_be_followed_by_only_one_newline():
+ pass
+elif os.name == "nt":
+ try:
+ import msvcrt
+ def i_should_be_followed_by_only_one_newline():
+ pass
+
+ except ImportError:
+
+ def i_should_be_followed_by_only_one_newline():
+ pass
+
+elif False:
+
+ class IHopeYouAreHavingALovelyDay:
+ def __call__(self):
+ print("i_should_be_followed_by_only_one_newline")
+else:
+
+ def foo():
+ pass
+
+with hmm_but_this_should_get_two_preceding_newlines():
+ pass
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function_trailing_comma.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function_trailing_comma.py
new file mode 100644
index 0000000000..7b01a0d801
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function_trailing_comma.py
@@ -0,0 +1,61 @@
+def f(a,):
+ d = {'key': 'value',}
+ tup = (1,)
+
+def f2(a,b,):
+ d = {'key': 'value', 'key2': 'value2',}
+ tup = (1,2,)
+
+def f(a:int=1,):
+ call(arg={'explode': 'this',})
+ call2(arg=[1,2,3],)
+ x = {
+ "a": 1,
+ "b": 2,
+ }["a"]
+ if a == {"a": 1,"b": 2,"c": 3,"d": 4,"e": 5,"f": 6,"g": 7,"h": 8,}["a"]:
+ pass
+
+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+]:
+ json = {"k": {"k2": {"k3": [1,]}}}
+
+
+
+# The type annotation shouldn't get a trailing comma since that would change its type.
+# Relevant bug report: https://github.com/psf/black/issues/2381.
+def some_function_with_a_really_long_name() -> (
+ returning_a_deeply_nested_import_of_a_type_i_suppose
+):
+ pass
+
+
+def some_method_with_a_really_long_name(very_long_parameter_so_yeah: str, another_long_parameter: int) -> (
+ another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not
+):
+ pass
+
+
+def func() -> (
+ also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black(this_shouldn_t_get_a_trailing_comma_too)
+):
+ pass
+
+
+def func() -> ((also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black(
+ this_shouldn_t_get_a_trailing_comma_too
+ ))
+):
+ pass
+
+
+# Make sure inner one-element tuple won't explode
+some_module.some_function(
+ argument1, (one_element_tuple,), argument4, argument5, argument6
+)
+
+# Inner trailing comma causes outer to explode
+some_module.some_function(
+ argument1, (one, two,), argument4, argument5, argument6
+)
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/import_spacing.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/import_spacing.py
new file mode 100644
index 0000000000..0a714bccda
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/import_spacing.py
@@ -0,0 +1,49 @@
+"""The asyncio package, tracking PEP 3156."""
+
+# flake8: noqa
+
+from logging import (
+ WARNING
+)
+from logging import (
+ ERROR,
+)
+import sys
+
+# This relies on each of the submodules having an __all__ variable.
+from .base_events import *
+from .coroutines import *
+from .events import * # comment here
+
+from .futures import *
+from .locks import * # comment here
+from .protocols import *
+
+from ..runners import * # comment here
+from ..queues import *
+from ..streams import *
+
+from some_library import (
+ Just, Enough, Libraries, To, Fit, In, This, Nice, Split, Which, We, No, Longer, Use
+)
+from name_of_a_company.extremely_long_project_name.component.ttypes import CuteLittleServiceHandlerFactoryyy
+from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import *
+
+from .a.b.c.subprocess import *
+from . import (tasks)
+from . import (A, B, C)
+from . import SomeVeryLongNameAndAllOfItsAdditionalLetters1, \
+ SomeVeryLongNameAndAllOfItsAdditionalLetters2
+
+__all__ = (
+ base_events.__all__
+ + coroutines.__all__
+ + events.__all__
+ + futures.__all__
+ + locks.__all__
+ + protocols.__all__
+ + runners.__all__
+ + queues.__all__
+ + streams.__all__
+ + tasks.__all__
+)
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/power_op_spacing.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/power_op_spacing.py
new file mode 100644
index 0000000000..1ae3fc2b4f
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/power_op_spacing.py
@@ -0,0 +1,63 @@
+def function(**kwargs):
+ t = a**2 + b**3
+ return t ** 2
+
+
+def function_replace_spaces(**kwargs):
+ t = a **2 + b** 3 + c ** 4
+
+
+def function_dont_replace_spaces():
+ {**a, **b, **c}
+
+
+a = 5**~4
+b = 5 ** f()
+c = -(5**2)
+d = 5 ** f["hi"]
+e = lazy(lambda **kwargs: 5)
+f = f() ** 5
+g = a.b**c.d
+h = 5 ** funcs.f()
+i = funcs.f() ** 5
+j = super().name ** 5
+k = [(2**idx, value) for idx, value in pairs]
+l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
+m = [([2**63], [1, 2**63])]
+n = count <= 10**5
+o = settings(max_examples=10**6)
+p = {(k, k**2): v**2 for k, v in pairs}
+q = [10**i for i in range(6)]
+r = x**y
+
+a = 5.0**~4.0
+b = 5.0 ** f()
+c = -(5.0**2.0)
+d = 5.0 ** f["hi"]
+e = lazy(lambda **kwargs: 5)
+f = f() ** 5.0
+g = a.b**c.d
+h = 5.0 ** funcs.f()
+i = funcs.f() ** 5.0
+j = super().name ** 5.0
+k = [(2.0**idx, value) for idx, value in pairs]
+l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
+m = [([2.0**63.0], [1.0, 2**63.0])]
+n = count <= 10**5.0
+o = settings(max_examples=10**6.0)
+p = {(k, k**2): v**2.0 for k, v in pairs}
+q = [10.5**i for i in range(6)]
+
+
+# WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873)
+if hasattr(view, "sum_of_weights"):
+ return np.divide( # type: ignore[no-any-return]
+ view.variance, # type: ignore[union-attr]
+ view.sum_of_weights, # type: ignore[union-attr]
+ out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr]
+ where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr]
+ )
+
+return np.divide(
+ where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore
+)
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/remove_parens.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/remove_parens.py
new file mode 100644
index 0000000000..ad1052eef9
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/remove_parens.py
@@ -0,0 +1,55 @@
+x = (1)
+x = (1.2)
+
+data = (
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+).encode()
+
+async def show_status():
+ while True:
+ try:
+ if report_host:
+ data = (
+ f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ ).encode()
+ except Exception as e:
+ pass
+
+def example():
+ return (("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"))
+
+
+def example1():
+ return ((1111111111111111111111111111111111111111111111111111111111111111111111111111111111111))
+
+
+def example1point5():
+ return ((((((1111111111111111111111111111111111111111111111111111111111111111111111111111111111111))))))
+
+
+def example2():
+ return (("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"))
+
+
+def example3():
+ return ((1111111111111111111111111111111111111111111111111111111111111111111111111111111))
+
+
+def example4():
+ return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((True))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
+
+
+def example5():
+ return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((()))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
+
+
+def example6():
+ return ((((((((({a:a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]})))))))))
+
+
+def example7():
+ return ((((((((({a:a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20000000000000000000]})))))))))
+
+
+def example8():
+ return (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((None)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py
new file mode 100644
index 0000000000..165117cdcb
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py
@@ -0,0 +1,31 @@
+slice[a.b : c.d]
+slice[d :: d + 1]
+slice[d + 1 :: d]
+slice[d::d]
+slice[0]
+slice[-1]
+slice[:-1]
+slice[::-1]
+slice[:c, c - 1]
+slice[c, c + 1, d::]
+slice[ham[c::d] :: 1]
+slice[ham[cheese**2 : -1] : 1 : 1, ham[1:2]]
+slice[:-1:]
+slice[lambda: None : lambda: None]
+slice[lambda x, y, *args, really=2, **kwargs: None :, None::]
+slice[1 or 2 : True and False]
+slice[not so_simple : 1 < val <= 10]
+slice[(1 for i in range(42)) : x]
+slice[:: [i for i in range(42)]]
+
+
+async def f():
+ slice[await x : [i async for i in arange(42)] : 42]
+
+
+# These are from PEP-8:
+ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:]
+ham[lower:upper], ham[lower:upper:], ham[lower::step]
+# ham[lower+offset : upper+offset]
+ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)]
+ham[lower + offset : upper + offset]
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/string_prefixes.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/string_prefixes.py
new file mode 100644
index 0000000000..80318fc6fb
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/string_prefixes.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+
+name = "Łukasz"
+(f"hello {name}", F"hello {name}")
+(b"", B"")
+(u"", U"")
+(r"", R"")
+
+(rf"", fr"", Rf"", fR"", rF"", Fr"", RF"", FR"")
+(rb"", br"", Rb"", bR"", rB"", Br"", RB"", BR"")
+
+
+def docstring_singleline():
+ R"""2020 was one hell of a year. The good news is that we were able to"""
+
+
+def docstring_multiline():
+ R"""
+ clear out all of the issues opened in that time :p
+ """
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/torture.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/torture.py
new file mode 100644
index 0000000000..42e4ef52cb
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/torture.py
@@ -0,0 +1,29 @@
+importA;() << 0 ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 #
+
+assert sort_by_dependency(
+ {
+ "1": {"2", "3"}, "2": {"2a", "2b"}, "3": {"3a", "3b"},
+ "2a": set(), "2b": set(), "3a": set(), "3b": set()
+ }
+) == ["2a", "2b", "2", "3a", "3b", "3", "1"]
+
+importA
+0;0^0#
+
+class A:
+ def foo(self):
+ for _ in range(10):
+ aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc( # pylint: disable=no-member
+ xxxxxxxxxxxx
+ )
+
+def test(self, othr):
+ return (1 == 2 and
+ (name, description, self.default, self.selected, self.auto_generated, self.parameters, self.meta_data, self.schedule) ==
+ (name, description, othr.default, othr.selected, othr.auto_generated, othr.parameters, othr.meta_data, othr.schedule))
+
+
+assert (
+ a_function(very_long_arguments_that_surpass_the_limit, which_is_eighty_eight_in_this_case_plus_a_bit_more)
+ == {"x": "this need to pass the line limit as well", "b": "but only by a little bit"}
+)
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens1.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens1.py
new file mode 100644
index 0000000000..1951cc8d2a
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens1.py
@@ -0,0 +1,25 @@
+if e1234123412341234.winerror not in (_winapi.ERROR_SEM_TIMEOUT,
+ _winapi.ERROR_PIPE_BUSY) or _check_timeout(t):
+ pass
+
+if x:
+ if y:
+ new_id = max(Vegetable.objects.order_by('-id')[0].id,
+ Mineral.objects.order_by('-id')[0].id) + 1
+
+class X:
+ def get_help_text(self):
+ return ngettext(
+ "Your password must contain at least %(min_length)d character.",
+ "Your password must contain at least %(min_length)d characters.",
+ self.min_length,
+ ) % {'min_length': self.min_length}
+
+class A:
+ def b(self):
+ if self.connection.mysql_is_mariadb and (
+ 10,
+ 4,
+ 3,
+ ) < self.connection.mysql_version < (10, 5, 2):
+ pass
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens2.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens2.py
new file mode 100644
index 0000000000..271371bd59
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens2.py
@@ -0,0 +1,3 @@
+if (e123456.get_tk_patchlevel() >= (8, 6, 0, 'final') or
+ (8, 5, 8) <= get_tk_patchlevel() < (8, 6)):
+ pass
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens3.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens3.py
new file mode 100644
index 0000000000..e5b4b7c4dc
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens3.py
@@ -0,0 +1,8 @@
+if True:
+ if True:
+ if True:
+ return _(
+ "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas "
+ + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.",
+ "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe",
+ ) % {"reported_username": reported_username, "report_reason": report_reason}
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/tricky_unicode_symbols.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/tricky_unicode_symbols.py
new file mode 100644
index 0000000000..ad8b610859
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/tricky_unicode_symbols.py
@@ -0,0 +1,9 @@
+ä = 1
+µ = 2
+蟒 = 3
+x󠄀 = 4
+មុ = 1
+Q̇_per_meter = 4
+
+A᧚ = 3
+A፩ = 8
diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/tupleassign.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/tupleassign.py
new file mode 100644
index 0000000000..513c24afdb
--- /dev/null
+++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/tupleassign.py
@@ -0,0 +1,7 @@
+# This is a standalone comment.
+sdfjklsdfsjldkflkjsf, sdfjsdfjlksdljkfsdlkf, sdfsdjfklsdfjlksdljkf, sdsfsdfjskdflsfsdf = 1, 2, 3
+
+# This is as well.
+this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890")
+
+(a,) = call()
diff --git a/crates/ruff_python_formatter/src/attachment.rs b/crates/ruff_python_formatter/src/attachment.rs
new file mode 100644
index 0000000000..8c52cfbf0c
--- /dev/null
+++ b/crates/ruff_python_formatter/src/attachment.rs
@@ -0,0 +1,31 @@
+use crate::core::visitor;
+use crate::core::visitor::Visitor;
+use crate::cst::{Expr, Stmt};
+use crate::trivia::{decorate_trivia, TriviaIndex, TriviaToken};
+
+struct AttachmentVisitor {
+ index: TriviaIndex,
+}
+
+impl<'a> Visitor<'a> for AttachmentVisitor {
+ fn visit_stmt(&mut self, stmt: &'a mut Stmt) {
+ let trivia = self.index.stmt.remove(&stmt.id());
+ if let Some(comments) = trivia {
+ stmt.trivia.extend(comments);
+ }
+ visitor::walk_stmt(self, stmt);
+ }
+
+ fn visit_expr(&mut self, expr: &'a mut Expr) {
+ let trivia = self.index.expr.remove(&expr.id());
+ if let Some(comments) = trivia {
+ expr.trivia.extend(comments);
+ }
+ visitor::walk_expr(self, expr);
+ }
+}
+
+pub fn attach(python_cst: &mut [Stmt], trivia: Vec) {
+ let index = decorate_trivia(trivia, python_cst);
+ AttachmentVisitor { index }.visit_body(python_cst);
+}
diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs
new file mode 100644
index 0000000000..6ea2d4c1e0
--- /dev/null
+++ b/crates/ruff_python_formatter/src/builders.rs
@@ -0,0 +1,77 @@
+use ruff_formatter::prelude::*;
+use ruff_formatter::{write, Format};
+use ruff_text_size::TextRange;
+
+use crate::context::ASTFormatContext;
+use crate::core::types::Range;
+use crate::trivia::{Relationship, Trivia, TriviaKind};
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub struct Literal {
+ range: Range,
+}
+
+impl Format> for Literal {
+ fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> {
+ let (text, start, end) = f.context().locator().slice(self.range);
+ f.write_element(FormatElement::StaticTextSlice {
+ text,
+ range: TextRange::new(start.try_into().unwrap(), end.try_into().unwrap()),
+ })
+ }
+}
+
+// TODO(charlie): We still can't use this everywhere we'd like. We need the AST
+// to include ranges for all `Ident` nodes.
+#[inline]
+pub const fn literal(range: Range) -> Literal {
+ Literal { range }
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub struct LeadingComments<'a> {
+ comments: &'a [Trivia],
+}
+
+impl Format> for LeadingComments<'_> {
+ fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> {
+ for comment in self.comments {
+ if matches!(comment.relationship, Relationship::Leading) {
+ if let TriviaKind::StandaloneComment(range) = comment.kind {
+ write!(f, [hard_line_break()])?;
+ write!(f, [literal(range)])?;
+ }
+ }
+ }
+ Ok(())
+ }
+}
+
+#[inline]
+pub const fn leading_comments(comments: &[Trivia]) -> LeadingComments {
+ LeadingComments { comments }
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub struct TrailingComments<'a> {
+ comments: &'a [Trivia],
+}
+
+impl Format> for TrailingComments<'_> {
+ fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> {
+ for comment in self.comments {
+ if matches!(comment.relationship, Relationship::Trailing) {
+ if let TriviaKind::StandaloneComment(range) = comment.kind {
+ write!(f, [hard_line_break()])?;
+ write!(f, [literal(range)])?;
+ }
+ }
+ }
+ Ok(())
+ }
+}
+
+#[inline]
+pub const fn trailing_comments(comments: &[Trivia]) -> TrailingComments {
+ TrailingComments { comments }
+}
diff --git a/crates/ruff_python_formatter/src/cli.rs b/crates/ruff_python_formatter/src/cli.rs
new file mode 100644
index 0000000000..87d7dc06aa
--- /dev/null
+++ b/crates/ruff_python_formatter/src/cli.rs
@@ -0,0 +1,11 @@
+use std::path::PathBuf;
+
+use clap::{command, Parser};
+
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+pub struct Cli {
+ /// Python file to round-trip.
+ #[arg(required = true)]
+ pub file: PathBuf,
+}
diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs
new file mode 100644
index 0000000000..75adddeaa5
--- /dev/null
+++ b/crates/ruff_python_formatter/src/context.rs
@@ -0,0 +1,28 @@
+use ruff_formatter::{FormatContext, SimpleFormatOptions};
+
+use crate::core::locator::Locator;
+
+pub struct ASTFormatContext<'a> {
+ options: SimpleFormatOptions,
+ locator: Locator<'a>,
+}
+
+impl<'a> ASTFormatContext<'a> {
+ pub fn new(options: SimpleFormatOptions, locator: Locator<'a>) -> Self {
+ Self { options, locator }
+ }
+}
+
+impl FormatContext for ASTFormatContext<'_> {
+ type Options = SimpleFormatOptions;
+
+ fn options(&self) -> &Self::Options {
+ &self.options
+ }
+}
+
+impl<'a> ASTFormatContext<'a> {
+ pub fn locator(&'a self) -> &'a Locator {
+ &self.locator
+ }
+}
diff --git a/crates/ruff_python_formatter/src/core/locator.rs b/crates/ruff_python_formatter/src/core/locator.rs
new file mode 100644
index 0000000000..7147df5f4d
--- /dev/null
+++ b/crates/ruff_python_formatter/src/core/locator.rs
@@ -0,0 +1,153 @@
+//! Struct used to efficiently slice source code at (row, column) Locations.
+
+use std::rc::Rc;
+
+use once_cell::unsync::OnceCell;
+use rustpython_parser::ast::Location;
+
+use crate::core::types::Range;
+
+pub struct Locator<'a> {
+ contents: &'a str,
+ contents_rc: Rc,
+ index: OnceCell,
+}
+
+pub enum Index {
+ Ascii(Vec),
+ Utf8(Vec>),
+}
+
+/// Compute the starting byte index of each line in ASCII source code.
+fn index_ascii(contents: &str) -> Vec {
+ let mut index = Vec::with_capacity(48);
+ index.push(0);
+ let bytes = contents.as_bytes();
+ for (i, byte) in bytes.iter().enumerate() {
+ if *byte == b'\n' {
+ index.push(i + 1);
+ }
+ }
+ index
+}
+
+/// Compute the starting byte index of each character in UTF-8 source code.
+fn index_utf8(contents: &str) -> Vec> {
+ let mut index = Vec::with_capacity(48);
+ let mut current_row = Vec::with_capacity(48);
+ let mut current_byte_offset = 0;
+ let mut previous_char = '\0';
+ for char in contents.chars() {
+ current_row.push(current_byte_offset);
+ if char == '\n' {
+ if previous_char == '\r' {
+ current_row.pop();
+ }
+ index.push(current_row);
+ current_row = Vec::with_capacity(48);
+ }
+ current_byte_offset += char.len_utf8();
+ previous_char = char;
+ }
+ index.push(current_row);
+ index
+}
+
+/// Compute the starting byte index of each line in source code.
+pub fn index(contents: &str) -> Index {
+ if contents.is_ascii() {
+ Index::Ascii(index_ascii(contents))
+ } else {
+ Index::Utf8(index_utf8(contents))
+ }
+}
+
+/// Truncate a [`Location`] to a byte offset in ASCII source code.
+fn truncate_ascii(location: Location, index: &[usize], contents: &str) -> usize {
+ if location.row() - 1 == index.len() && location.column() == 0
+ || (!index.is_empty()
+ && location.row() - 1 == index.len() - 1
+ && index[location.row() - 1] + location.column() >= contents.len())
+ {
+ contents.len()
+ } else {
+ index[location.row() - 1] + location.column()
+ }
+}
+
+/// Truncate a [`Location`] to a byte offset in UTF-8 source code.
+fn truncate_utf8(location: Location, index: &[Vec], contents: &str) -> usize {
+ if (location.row() - 1 == index.len() && location.column() == 0)
+ || (location.row() - 1 == index.len() - 1
+ && location.column() == index[location.row() - 1].len())
+ {
+ contents.len()
+ } else {
+ index[location.row() - 1][location.column()]
+ }
+}
+
+/// Truncate a [`Location`] to a byte offset in source code.
+fn truncate(location: Location, index: &Index, contents: &str) -> usize {
+ match index {
+ Index::Ascii(index) => truncate_ascii(location, index, contents),
+ Index::Utf8(index) => truncate_utf8(location, index, contents),
+ }
+}
+
+impl<'a> Locator<'a> {
+ pub fn new(contents: &'a str) -> Self {
+ Locator {
+ contents,
+ contents_rc: Rc::from(contents),
+ index: OnceCell::new(),
+ }
+ }
+
+ fn get_or_init_index(&self) -> &Index {
+ self.index.get_or_init(|| index(self.contents))
+ }
+
+ pub fn slice_source_code_until(&self, location: Location) -> &'a str {
+ let index = self.get_or_init_index();
+ let offset = truncate(location, index, self.contents);
+ &self.contents[..offset]
+ }
+
+ pub fn slice_source_code_at(&self, location: Location) -> &'a str {
+ let index = self.get_or_init_index();
+ let offset = truncate(location, index, self.contents);
+ &self.contents[offset..]
+ }
+
+ pub fn slice_source_code_range(&self, range: &Range) -> &'a str {
+ let index = self.get_or_init_index();
+ let start = truncate(range.location, index, self.contents);
+ let end = truncate(range.end_location, index, self.contents);
+ &self.contents[start..end]
+ }
+
+ pub fn slice(&self, range: Range) -> (Rc, usize, usize) {
+ let index = self.get_or_init_index();
+ let start = truncate(range.location, index, self.contents);
+ let end = truncate(range.end_location, index, self.contents);
+ (Rc::clone(&self.contents_rc), start, end)
+ }
+
+ pub fn partition_source_code_at(
+ &self,
+ outer: &Range,
+ inner: &Range,
+ ) -> (&'a str, &'a str, &'a str) {
+ let index = self.get_or_init_index();
+ let outer_start = truncate(outer.location, index, self.contents);
+ let outer_end = truncate(outer.end_location, index, self.contents);
+ let inner_start = truncate(inner.location, index, self.contents);
+ let inner_end = truncate(inner.end_location, index, self.contents);
+ (
+ &self.contents[outer_start..inner_start],
+ &self.contents[inner_start..inner_end],
+ &self.contents[inner_end..outer_end],
+ )
+ }
+}
diff --git a/crates/ruff_python_formatter/src/core/mod.rs b/crates/ruff_python_formatter/src/core/mod.rs
new file mode 100644
index 0000000000..ac97bd01f3
--- /dev/null
+++ b/crates/ruff_python_formatter/src/core/mod.rs
@@ -0,0 +1,4 @@
+pub mod locator;
+pub mod rustpython_helpers;
+pub mod types;
+pub mod visitor;
diff --git a/crates/ruff_python_formatter/src/core/rustpython_helpers.rs b/crates/ruff_python_formatter/src/core/rustpython_helpers.rs
new file mode 100644
index 0000000000..eb498f7ebc
--- /dev/null
+++ b/crates/ruff_python_formatter/src/core/rustpython_helpers.rs
@@ -0,0 +1,29 @@
+use rustpython_parser::ast::{Mod, Suite};
+use rustpython_parser::error::ParseError;
+use rustpython_parser::lexer::LexResult;
+use rustpython_parser::mode::Mode;
+use rustpython_parser::{lexer, parser};
+
+/// Collect tokens up to and including the first error.
+pub fn tokenize(contents: &str) -> Vec {
+ let mut tokens: Vec = vec![];
+ for tok in lexer::make_tokenizer(contents) {
+ let is_err = tok.is_err();
+ tokens.push(tok);
+ if is_err {
+ break;
+ }
+ }
+ tokens
+}
+
+/// Parse a full Python program from its tokens.
+pub(crate) fn parse_program_tokens(
+ lxr: Vec,
+ source_path: &str,
+) -> anyhow::Result {
+ parser::parse_tokens(lxr, Mode::Module, source_path).map(|top| match top {
+ Mod::Module { body, .. } => body,
+ _ => unreachable!(),
+ })
+}
diff --git a/crates/ruff_python_formatter/src/core/types.rs b/crates/ruff_python_formatter/src/core/types.rs
new file mode 100644
index 0000000000..e1f1a49041
--- /dev/null
+++ b/crates/ruff_python_formatter/src/core/types.rs
@@ -0,0 +1,76 @@
+use std::ops::Deref;
+
+use rustpython_parser::ast::Location;
+
+use crate::cst::{Expr, Located, Stmt};
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Range {
+ pub location: Location,
+ pub end_location: Location,
+}
+
+impl Range {
+ pub fn new(location: Location, end_location: Location) -> Self {
+ Self {
+ location,
+ end_location,
+ }
+ }
+
+ pub fn from_located(located: &Located) -> Self {
+ Range::new(located.location, located.end_location.unwrap())
+ }
+}
+
+#[derive(Debug, Copy, Clone)]
+pub struct RefEquality<'a, T>(pub &'a T);
+
+impl<'a, T> std::hash::Hash for RefEquality<'a, T> {
+ fn hash(&self, state: &mut H)
+ where
+ H: std::hash::Hasher,
+ {
+ (self.0 as *const T).hash(state);
+ }
+}
+
+impl<'a, 'b, T> PartialEq> for RefEquality<'a, T> {
+ fn eq(&self, other: &RefEquality<'b, T>) -> bool {
+ std::ptr::eq(self.0, other.0)
+ }
+}
+
+impl<'a, T> Eq for RefEquality<'a, T> {}
+
+impl<'a, T> Deref for RefEquality<'a, T> {
+ type Target = T;
+
+ fn deref(&self) -> &T {
+ self.0
+ }
+}
+
+impl<'a> From<&RefEquality<'a, Stmt>> for &'a Stmt {
+ fn from(r: &RefEquality<'a, Stmt>) -> Self {
+ r.0
+ }
+}
+
+impl<'a> From<&RefEquality<'a, Expr>> for &'a Expr {
+ fn from(r: &RefEquality<'a, Expr>) -> Self {
+ r.0
+ }
+}
+
+impl<'a> From<&RefEquality<'a, rustpython_parser::ast::Stmt>> for &'a rustpython_parser::ast::Stmt {
+ fn from(r: &RefEquality<'a, rustpython_parser::ast::Stmt>) -> Self {
+ r.0
+ }
+}
+
+impl<'a> From<&RefEquality<'a, rustpython_parser::ast::Expr>> for &'a rustpython_parser::ast::Expr {
+ fn from(r: &RefEquality<'a, rustpython_parser::ast::Expr>) -> Self {
+ r.0
+ }
+}
diff --git a/crates/ruff_python_formatter/src/core/visitor.rs b/crates/ruff_python_formatter/src/core/visitor.rs
new file mode 100644
index 0000000000..e562609c52
--- /dev/null
+++ b/crates/ruff_python_formatter/src/core/visitor.rs
@@ -0,0 +1,574 @@
+use rustpython_parser::ast::Constant;
+
+use crate::cst::{
+ Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Excepthandler, ExcepthandlerKind, Expr,
+ ExprContext, ExprKind, Keyword, MatchCase, Operator, Pattern, PatternKind, Stmt, StmtKind,
+ Unaryop, Withitem,
+};
+
+pub trait Visitor<'a> {
+ fn visit_stmt(&mut self, stmt: &'a mut Stmt) {
+ walk_stmt(self, stmt);
+ }
+ fn visit_annotation(&mut self, expr: &'a mut Expr) {
+ walk_expr(self, expr);
+ }
+ fn visit_expr(&mut self, expr: &'a mut Expr) {
+ walk_expr(self, expr);
+ }
+ fn visit_constant(&mut self, constant: &'a mut Constant) {
+ walk_constant(self, constant);
+ }
+ fn visit_expr_context(&mut self, expr_context: &'a mut ExprContext) {
+ walk_expr_context(self, expr_context);
+ }
+ fn visit_boolop(&mut self, boolop: &'a mut Boolop) {
+ walk_boolop(self, boolop);
+ }
+ fn visit_operator(&mut self, operator: &'a mut Operator) {
+ walk_operator(self, operator);
+ }
+ fn visit_unaryop(&mut self, unaryop: &'a mut Unaryop) {
+ walk_unaryop(self, unaryop);
+ }
+ fn visit_cmpop(&mut self, cmpop: &'a mut Cmpop) {
+ walk_cmpop(self, cmpop);
+ }
+ fn visit_comprehension(&mut self, comprehension: &'a mut Comprehension) {
+ walk_comprehension(self, comprehension);
+ }
+ fn visit_excepthandler(&mut self, excepthandler: &'a mut Excepthandler) {
+ walk_excepthandler(self, excepthandler);
+ }
+ fn visit_format_spec(&mut self, format_spec: &'a mut Expr) {
+ walk_expr(self, format_spec);
+ }
+ fn visit_arguments(&mut self, arguments: &'a mut Arguments) {
+ walk_arguments(self, arguments);
+ }
+ fn visit_arg(&mut self, arg: &'a mut Arg) {
+ walk_arg(self, arg);
+ }
+ fn visit_keyword(&mut self, keyword: &'a mut Keyword) {
+ walk_keyword(self, keyword);
+ }
+ fn visit_alias(&mut self, alias: &'a mut Alias) {
+ walk_alias(self, alias);
+ }
+ fn visit_withitem(&mut self, withitem: &'a mut Withitem) {
+ walk_withitem(self, withitem);
+ }
+ fn visit_match_case(&mut self, match_case: &'a mut MatchCase) {
+ walk_match_case(self, match_case);
+ }
+ fn visit_pattern(&mut self, pattern: &'a mut Pattern) {
+ walk_pattern(self, pattern);
+ }
+ fn visit_body(&mut self, body: &'a mut [Stmt]) {
+ walk_body(self, body);
+ }
+}
+
+pub fn walk_body<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, body: &'a mut [Stmt]) {
+ for stmt in body {
+ visitor.visit_stmt(stmt);
+ }
+}
+
+pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a mut Stmt) {
+ match &mut stmt.node {
+ StmtKind::FunctionDef {
+ args,
+ body,
+ decorator_list,
+ returns,
+ ..
+ } => {
+ visitor.visit_arguments(args);
+ for expr in decorator_list {
+ visitor.visit_expr(expr);
+ }
+ for expr in returns {
+ visitor.visit_annotation(expr);
+ }
+ visitor.visit_body(body);
+ }
+ StmtKind::AsyncFunctionDef {
+ args,
+ body,
+ decorator_list,
+ returns,
+ ..
+ } => {
+ visitor.visit_arguments(args);
+ for expr in decorator_list {
+ visitor.visit_expr(expr);
+ }
+ for expr in returns {
+ visitor.visit_annotation(expr);
+ }
+ visitor.visit_body(body);
+ }
+ StmtKind::ClassDef {
+ bases,
+ keywords,
+ body,
+ decorator_list,
+ ..
+ } => {
+ for expr in bases {
+ visitor.visit_expr(expr);
+ }
+ for keyword in keywords {
+ visitor.visit_keyword(keyword);
+ }
+ for expr in decorator_list {
+ visitor.visit_expr(expr);
+ }
+ visitor.visit_body(body);
+ }
+ StmtKind::Return { value } => {
+ if let Some(expr) = value {
+ visitor.visit_expr(expr);
+ }
+ }
+ StmtKind::Delete { targets } => {
+ for expr in targets {
+ visitor.visit_expr(expr);
+ }
+ }
+ StmtKind::Assign { targets, value, .. } => {
+ visitor.visit_expr(value);
+ for expr in targets {
+ visitor.visit_expr(expr);
+ }
+ }
+ StmtKind::AugAssign { target, op, value } => {
+ visitor.visit_expr(target);
+ visitor.visit_operator(op);
+ visitor.visit_expr(value);
+ }
+ StmtKind::AnnAssign {
+ target,
+ annotation,
+ value,
+ ..
+ } => {
+ visitor.visit_annotation(annotation);
+ if let Some(expr) = value {
+ visitor.visit_expr(expr);
+ }
+ visitor.visit_expr(target);
+ }
+ StmtKind::For {
+ target,
+ iter,
+ body,
+ orelse,
+ ..
+ } => {
+ visitor.visit_expr(iter);
+ visitor.visit_expr(target);
+ visitor.visit_body(body);
+ visitor.visit_body(orelse);
+ }
+ StmtKind::AsyncFor {
+ target,
+ iter,
+ body,
+ orelse,
+ ..
+ } => {
+ visitor.visit_expr(iter);
+ visitor.visit_expr(target);
+ visitor.visit_body(body);
+ visitor.visit_body(orelse);
+ }
+ StmtKind::While { test, body, orelse } => {
+ visitor.visit_expr(test);
+ visitor.visit_body(body);
+ visitor.visit_body(orelse);
+ }
+ StmtKind::If { test, body, orelse } => {
+ visitor.visit_expr(test);
+ visitor.visit_body(body);
+ visitor.visit_body(orelse);
+ }
+ StmtKind::With { items, body, .. } => {
+ for withitem in items {
+ visitor.visit_withitem(withitem);
+ }
+ visitor.visit_body(body);
+ }
+ StmtKind::AsyncWith { items, body, .. } => {
+ for withitem in items {
+ visitor.visit_withitem(withitem);
+ }
+ visitor.visit_body(body);
+ }
+ StmtKind::Match { subject, cases } => {
+ // TODO(charlie): Handle `cases`.
+ visitor.visit_expr(subject);
+ for match_case in cases {
+ visitor.visit_match_case(match_case);
+ }
+ }
+ StmtKind::Raise { exc, cause } => {
+ if let Some(expr) = exc {
+ visitor.visit_expr(expr);
+ };
+ if let Some(expr) = cause {
+ visitor.visit_expr(expr);
+ };
+ }
+ StmtKind::Try {
+ body,
+ handlers,
+ orelse,
+ finalbody,
+ } => {
+ visitor.visit_body(body);
+ for excepthandler in handlers {
+ visitor.visit_excepthandler(excepthandler);
+ }
+ visitor.visit_body(orelse);
+ visitor.visit_body(finalbody);
+ }
+ StmtKind::Assert { test, msg } => {
+ visitor.visit_expr(test);
+ if let Some(expr) = msg {
+ visitor.visit_expr(expr);
+ }
+ }
+ StmtKind::Import { names } => {
+ for alias in names {
+ visitor.visit_alias(alias);
+ }
+ }
+ StmtKind::ImportFrom { names, .. } => {
+ for alias in names {
+ visitor.visit_alias(alias);
+ }
+ }
+ StmtKind::Global { .. } => {}
+ StmtKind::Nonlocal { .. } => {}
+ StmtKind::Expr { value } => visitor.visit_expr(value),
+ StmtKind::Pass => {}
+ StmtKind::Break => {}
+ StmtKind::Continue => {}
+ }
+}
+
+pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a mut Expr) {
+ match &mut expr.node {
+ ExprKind::BoolOp { op, values } => {
+ visitor.visit_boolop(op);
+ for expr in values {
+ visitor.visit_expr(expr);
+ }
+ }
+ ExprKind::NamedExpr { target, value } => {
+ visitor.visit_expr(target);
+ visitor.visit_expr(value);
+ }
+ ExprKind::BinOp { left, op, right } => {
+ visitor.visit_expr(left);
+ visitor.visit_operator(op);
+ visitor.visit_expr(right);
+ }
+ ExprKind::UnaryOp { op, operand } => {
+ visitor.visit_unaryop(op);
+ visitor.visit_expr(operand);
+ }
+ ExprKind::Lambda { args, body } => {
+ visitor.visit_arguments(args);
+ visitor.visit_expr(body);
+ }
+ ExprKind::IfExp { test, body, orelse } => {
+ visitor.visit_expr(test);
+ visitor.visit_expr(body);
+ visitor.visit_expr(orelse);
+ }
+ ExprKind::Dict { keys, values } => {
+ for expr in keys.iter_mut().flatten() {
+ visitor.visit_expr(expr);
+ }
+ for expr in values {
+ visitor.visit_expr(expr);
+ }
+ }
+ ExprKind::Set { elts } => {
+ for expr in elts {
+ visitor.visit_expr(expr);
+ }
+ }
+ ExprKind::ListComp { elt, generators } => {
+ for comprehension in generators {
+ visitor.visit_comprehension(comprehension);
+ }
+ visitor.visit_expr(elt);
+ }
+ ExprKind::SetComp { elt, generators } => {
+ for comprehension in generators {
+ visitor.visit_comprehension(comprehension);
+ }
+ visitor.visit_expr(elt);
+ }
+ ExprKind::DictComp {
+ key,
+ value,
+ generators,
+ } => {
+ for comprehension in generators {
+ visitor.visit_comprehension(comprehension);
+ }
+ visitor.visit_expr(key);
+ visitor.visit_expr(value);
+ }
+ ExprKind::GeneratorExp { elt, generators } => {
+ for comprehension in generators {
+ visitor.visit_comprehension(comprehension);
+ }
+ visitor.visit_expr(elt);
+ }
+ ExprKind::Await { value } => visitor.visit_expr(value),
+ ExprKind::Yield { value } => {
+ if let Some(expr) = value {
+ visitor.visit_expr(expr);
+ }
+ }
+ ExprKind::YieldFrom { value } => visitor.visit_expr(value),
+ ExprKind::Compare {
+ left,
+ ops,
+ comparators,
+ } => {
+ visitor.visit_expr(left);
+ for cmpop in ops {
+ visitor.visit_cmpop(cmpop);
+ }
+ for expr in comparators {
+ visitor.visit_expr(expr);
+ }
+ }
+ ExprKind::Call {
+ func,
+ args,
+ keywords,
+ } => {
+ visitor.visit_expr(func);
+ for expr in args {
+ visitor.visit_expr(expr);
+ }
+ for keyword in keywords {
+ visitor.visit_keyword(keyword);
+ }
+ }
+ ExprKind::FormattedValue {
+ value, format_spec, ..
+ } => {
+ visitor.visit_expr(value);
+ if let Some(expr) = format_spec {
+ visitor.visit_format_spec(expr);
+ }
+ }
+ ExprKind::JoinedStr { values } => {
+ for expr in values {
+ visitor.visit_expr(expr);
+ }
+ }
+ ExprKind::Constant { value, .. } => visitor.visit_constant(value),
+ ExprKind::Attribute { value, ctx, .. } => {
+ visitor.visit_expr(value);
+ visitor.visit_expr_context(ctx);
+ }
+ ExprKind::Subscript { value, slice, ctx } => {
+ visitor.visit_expr(value);
+ visitor.visit_expr(slice);
+ visitor.visit_expr_context(ctx);
+ }
+ ExprKind::Starred { value, ctx } => {
+ visitor.visit_expr(value);
+ visitor.visit_expr_context(ctx);
+ }
+ ExprKind::Name { ctx, .. } => {
+ visitor.visit_expr_context(ctx);
+ }
+ ExprKind::List { elts, ctx } => {
+ for expr in elts {
+ visitor.visit_expr(expr);
+ }
+ visitor.visit_expr_context(ctx);
+ }
+ ExprKind::Tuple { elts, ctx } => {
+ for expr in elts {
+ visitor.visit_expr(expr);
+ }
+ visitor.visit_expr_context(ctx);
+ }
+ ExprKind::Slice { lower, upper, step } => {
+ if let Some(expr) = lower {
+ visitor.visit_expr(expr);
+ }
+ if let Some(expr) = upper {
+ visitor.visit_expr(expr);
+ }
+ if let Some(expr) = step {
+ visitor.visit_expr(expr);
+ }
+ }
+ }
+}
+
+pub fn walk_constant<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, constant: &'a mut Constant) {
+ if let Constant::Tuple(constants) = constant {
+ for constant in constants {
+ visitor.visit_constant(constant);
+ }
+ }
+}
+
+pub fn walk_comprehension<'a, V: Visitor<'a> + ?Sized>(
+ visitor: &mut V,
+ comprehension: &'a mut Comprehension,
+) {
+ visitor.visit_expr(&mut comprehension.iter);
+ visitor.visit_expr(&mut comprehension.target);
+ for expr in &mut comprehension.ifs {
+ visitor.visit_expr(expr);
+ }
+}
+
+pub fn walk_excepthandler<'a, V: Visitor<'a> + ?Sized>(
+ visitor: &mut V,
+ excepthandler: &'a mut Excepthandler,
+) {
+ match &mut excepthandler.node {
+ ExcepthandlerKind::ExceptHandler { type_, body, .. } => {
+ if let Some(expr) = type_ {
+ visitor.visit_expr(expr);
+ }
+ visitor.visit_body(body);
+ }
+ }
+}
+
+pub fn walk_arguments<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arguments: &'a mut Arguments) {
+ for arg in &mut arguments.posonlyargs {
+ visitor.visit_arg(arg);
+ }
+ for arg in &mut arguments.args {
+ visitor.visit_arg(arg);
+ }
+ if let Some(arg) = &mut arguments.vararg {
+ visitor.visit_arg(arg);
+ }
+ for arg in &mut arguments.kwonlyargs {
+ visitor.visit_arg(arg);
+ }
+ for expr in &mut arguments.kw_defaults {
+ visitor.visit_expr(expr);
+ }
+ if let Some(arg) = &mut arguments.kwarg {
+ visitor.visit_arg(arg);
+ }
+ for expr in &mut arguments.defaults {
+ visitor.visit_expr(expr);
+ }
+}
+
+pub fn walk_arg<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arg: &'a mut Arg) {
+ if let Some(expr) = &mut arg.node.annotation {
+ visitor.visit_annotation(expr);
+ }
+}
+
+pub fn walk_keyword<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, keyword: &'a mut Keyword) {
+ visitor.visit_expr(&mut keyword.node.value);
+}
+
+pub fn walk_withitem<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, withitem: &'a mut Withitem) {
+ visitor.visit_expr(&mut withitem.context_expr);
+ if let Some(expr) = &mut withitem.optional_vars {
+ visitor.visit_expr(expr);
+ }
+}
+
+pub fn walk_match_case<'a, V: Visitor<'a> + ?Sized>(
+ visitor: &mut V,
+ match_case: &'a mut MatchCase,
+) {
+ visitor.visit_pattern(&mut match_case.pattern);
+ if let Some(expr) = &mut match_case.guard {
+ visitor.visit_expr(expr);
+ }
+ visitor.visit_body(&mut match_case.body);
+}
+
+pub fn walk_pattern<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, pattern: &'a mut Pattern) {
+ match &mut pattern.node {
+ PatternKind::MatchValue { value } => visitor.visit_expr(value),
+ PatternKind::MatchSingleton { value } => visitor.visit_constant(value),
+ PatternKind::MatchSequence { patterns } => {
+ for pattern in patterns {
+ visitor.visit_pattern(pattern);
+ }
+ }
+ PatternKind::MatchMapping { keys, patterns, .. } => {
+ for expr in keys {
+ visitor.visit_expr(expr);
+ }
+ for pattern in patterns {
+ visitor.visit_pattern(pattern);
+ }
+ }
+ PatternKind::MatchClass {
+ cls,
+ patterns,
+ kwd_patterns,
+ ..
+ } => {
+ visitor.visit_expr(cls);
+ for pattern in patterns {
+ visitor.visit_pattern(pattern);
+ }
+
+ for pattern in kwd_patterns {
+ visitor.visit_pattern(pattern);
+ }
+ }
+ PatternKind::MatchStar { .. } => {}
+ PatternKind::MatchAs { pattern, .. } => {
+ if let Some(pattern) = pattern {
+ visitor.visit_pattern(pattern);
+ }
+ }
+ PatternKind::MatchOr { patterns } => {
+ for pattern in patterns {
+ visitor.visit_pattern(pattern);
+ }
+ }
+ }
+}
+
+#[allow(unused_variables)]
+pub fn walk_expr_context<'a, V: Visitor<'a> + ?Sized>(
+ visitor: &mut V,
+ expr_context: &'a mut ExprContext,
+) {
+}
+
+#[allow(unused_variables)]
+pub fn walk_boolop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, boolop: &'a mut Boolop) {}
+
+#[allow(unused_variables)]
+pub fn walk_operator<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, operator: &'a mut Operator) {}
+
+#[allow(unused_variables)]
+pub fn walk_unaryop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, unaryop: &'a mut Unaryop) {}
+
+#[allow(unused_variables)]
+pub fn walk_cmpop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, cmpop: &'a mut Cmpop) {}
+
+#[allow(unused_variables)]
+pub fn walk_alias<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, alias: &'a mut Alias) {}
diff --git a/crates/ruff_python_formatter/src/cst.rs b/crates/ruff_python_formatter/src/cst.rs
new file mode 100644
index 0000000000..24471ac76d
--- /dev/null
+++ b/crates/ruff_python_formatter/src/cst.rs
@@ -0,0 +1,1222 @@
+#![allow(clippy::derive_partial_eq_without_eq)]
+
+use rustpython_parser::ast::{Constant, Location};
+
+use crate::trivia::{Parenthesize, Trivia};
+
+type Ident = String;
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct Located {
+ pub location: Location,
+ pub end_location: Option,
+ pub node: T,
+ pub trivia: Vec,
+ pub parentheses: Parenthesize,
+}
+
+impl Located {
+ pub fn new(location: Location, end_location: Location, node: T) -> Self {
+ Self {
+ location,
+ end_location: Some(end_location),
+ node,
+ trivia: Vec::new(),
+ parentheses: Parenthesize::Never,
+ }
+ }
+
+ pub fn add_trivia(&mut self, trivia: Trivia) {
+ self.trivia.push(trivia);
+ }
+
+ pub fn id(&self) -> usize {
+ self as *const _ as usize
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum ExprContext {
+ Load,
+ Store,
+ Del,
+}
+
+impl From for ExprContext {
+ fn from(context: rustpython_parser::ast::ExprContext) -> Self {
+ match context {
+ rustpython_parser::ast::ExprContext::Load => Self::Load,
+ rustpython_parser::ast::ExprContext::Store => Self::Store,
+ rustpython_parser::ast::ExprContext::Del => Self::Del,
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum Boolop {
+ And,
+ Or,
+}
+
+impl From for Boolop {
+ fn from(op: rustpython_parser::ast::Boolop) -> Self {
+ match op {
+ rustpython_parser::ast::Boolop::And => Self::And,
+ rustpython_parser::ast::Boolop::Or => Self::Or,
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum Operator {
+ Add,
+ Sub,
+ Mult,
+ MatMult,
+ Div,
+ Mod,
+ Pow,
+ LShift,
+ RShift,
+ BitOr,
+ BitXor,
+ BitAnd,
+ FloorDiv,
+}
+
+impl From for Operator {
+ fn from(op: rustpython_parser::ast::Operator) -> Self {
+ match op {
+ rustpython_parser::ast::Operator::Add => Self::Add,
+ rustpython_parser::ast::Operator::Sub => Self::Sub,
+ rustpython_parser::ast::Operator::Mult => Self::Mult,
+ rustpython_parser::ast::Operator::MatMult => Self::MatMult,
+ rustpython_parser::ast::Operator::Div => Self::Div,
+ rustpython_parser::ast::Operator::Mod => Self::Mod,
+ rustpython_parser::ast::Operator::Pow => Self::Pow,
+ rustpython_parser::ast::Operator::LShift => Self::LShift,
+ rustpython_parser::ast::Operator::RShift => Self::RShift,
+ rustpython_parser::ast::Operator::BitOr => Self::BitOr,
+ rustpython_parser::ast::Operator::BitXor => Self::BitXor,
+ rustpython_parser::ast::Operator::BitAnd => Self::BitAnd,
+ rustpython_parser::ast::Operator::FloorDiv => Self::FloorDiv,
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum Unaryop {
+ Invert,
+ Not,
+ UAdd,
+ USub,
+}
+
+impl From for Unaryop {
+ fn from(op: rustpython_parser::ast::Unaryop) -> Self {
+ match op {
+ rustpython_parser::ast::Unaryop::Invert => Self::Invert,
+ rustpython_parser::ast::Unaryop::Not => Self::Not,
+ rustpython_parser::ast::Unaryop::UAdd => Self::UAdd,
+ rustpython_parser::ast::Unaryop::USub => Self::USub,
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum Cmpop {
+ Eq,
+ NotEq,
+ Lt,
+ LtE,
+ Gt,
+ GtE,
+ Is,
+ IsNot,
+ In,
+ NotIn,
+}
+
+impl From for Cmpop {
+ fn from(op: rustpython_parser::ast::Cmpop) -> Self {
+ match op {
+ rustpython_parser::ast::Cmpop::Eq => Self::Eq,
+ rustpython_parser::ast::Cmpop::NotEq => Self::NotEq,
+ rustpython_parser::ast::Cmpop::Lt => Self::Lt,
+ rustpython_parser::ast::Cmpop::LtE => Self::LtE,
+ rustpython_parser::ast::Cmpop::Gt => Self::Gt,
+ rustpython_parser::ast::Cmpop::GtE => Self::GtE,
+ rustpython_parser::ast::Cmpop::Is => Self::Is,
+ rustpython_parser::ast::Cmpop::IsNot => Self::IsNot,
+ rustpython_parser::ast::Cmpop::In => Self::In,
+ rustpython_parser::ast::Cmpop::NotIn => Self::NotIn,
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum StmtKind {
+ FunctionDef {
+ name: Ident,
+ args: Box,
+ body: Vec,
+ decorator_list: Vec,
+ returns: Option>,
+ type_comment: Option,
+ },
+ AsyncFunctionDef {
+ name: Ident,
+ args: Box,
+ body: Vec,
+ decorator_list: Vec,
+ returns: Option>,
+ type_comment: Option,
+ },
+ ClassDef {
+ name: Ident,
+ bases: Vec,
+ keywords: Vec,
+ body: Vec,
+ decorator_list: Vec,
+ },
+ Return {
+ value: Option,
+ },
+ Delete {
+ targets: Vec,
+ },
+ Assign {
+ targets: Vec,
+ value: Box,
+ type_comment: Option,
+ },
+ AugAssign {
+ target: Box,
+ op: Operator,
+ value: Box,
+ },
+ AnnAssign {
+ target: Box,
+ annotation: Box,
+ value: Option>,
+ simple: usize,
+ },
+ For {
+ target: Box,
+ iter: Box,
+ body: Vec,
+ orelse: Vec,
+ type_comment: Option,
+ },
+ AsyncFor {
+ target: Box,
+ iter: Box,
+ body: Vec,
+ orelse: Vec,
+ type_comment: Option,
+ },
+ While {
+ test: Box,
+ body: Vec,
+ orelse: Vec,
+ },
+ If {
+ test: Box,
+ body: Vec,
+ orelse: Vec,
+ },
+ With {
+ items: Vec,
+ body: Vec,
+ type_comment: Option,
+ },
+ AsyncWith {
+ items: Vec,
+ body: Vec,
+ type_comment: Option,
+ },
+ Match {
+ subject: Box,
+ cases: Vec,
+ },
+ Raise {
+ exc: Option>,
+ cause: Option>,
+ },
+ Try {
+ body: Vec,
+ handlers: Vec,
+ orelse: Vec,
+ finalbody: Vec,
+ },
+ Assert {
+ test: Box,
+ msg: Option>,
+ },
+ Import {
+ names: Vec,
+ },
+ ImportFrom {
+ module: Option,
+ names: Vec,
+ level: Option,
+ },
+ Global {
+ names: Vec,
+ },
+ Nonlocal {
+ names: Vec,
+ },
+ Expr {
+ value: Box,
+ },
+ Pass,
+ Break,
+ Continue,
+}
+
+pub type Stmt = Located;
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum ExprKind {
+ BoolOp {
+ op: Boolop,
+ values: Vec,
+ },
+ NamedExpr {
+ target: Box,
+ value: Box,
+ },
+ BinOp {
+ left: Box,
+ op: Operator,
+ right: Box,
+ },
+ UnaryOp {
+ op: Unaryop,
+ operand: Box,
+ },
+ Lambda {
+ args: Box,
+ body: Box,
+ },
+ IfExp {
+ test: Box,
+ body: Box,
+ orelse: Box,
+ },
+ Dict {
+ keys: Vec