Add support for basic Constant::Str formatting (#3173)

This PR enables us to apply the proper quotation marks, including support for escapes. There are some significant TODOs, especially around implicit concatenations like:

```py
(
  "abc"
  "def"
)
```

Which are represented as a single AST node, which requires us to tokenize _within_ the formatter to identify all the individual string parts.
This commit is contained in:
Charlie Marsh 2023-02-23 11:23:10 -05:00 committed by GitHub
parent 095f005bf4
commit f967f344fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 426 additions and 706 deletions

View file

@ -0,0 +1,23 @@
use ruff_python::string::{
SINGLE_QUOTE_PREFIXES, SINGLE_QUOTE_SUFFIXES, TRIPLE_QUOTE_PREFIXES, TRIPLE_QUOTE_SUFFIXES,
};
/// Return the leading quote string for a docstring (e.g., `"""`).
pub fn leading_quote(content: &str) -> Option<&str> {
if let Some(first_line) = content.lines().next() {
for pattern in TRIPLE_QUOTE_PREFIXES.iter().chain(SINGLE_QUOTE_PREFIXES) {
if first_line.starts_with(pattern) {
return Some(pattern);
}
}
}
None
}
/// Return the trailing quote string for a docstring (e.g., `"""`).
pub fn trailing_quote(content: &str) -> Option<&&str> {
TRIPLE_QUOTE_SUFFIXES
.iter()
.chain(SINGLE_QUOTE_SUFFIXES)
.find(|&pattern| content.ends_with(pattern))
}

View file

@ -1,3 +1,4 @@
pub mod helpers;
pub mod locator;
pub mod types;
pub mod visitor;

View file

@ -13,6 +13,7 @@ use crate::cst::{
Arguments, Boolop, Cmpop, Comprehension, Expr, ExprKind, Keyword, Operator, Unaryop,
};
use crate::format::helpers::{is_self_closing, is_simple_power, is_simple_slice};
use crate::format::strings::string_literal;
use crate::shared_traits::AsFormat;
use crate::trivia::{Parenthesize, Relationship, TriviaKind};
@ -128,8 +129,6 @@ fn format_tuple(
write!(
f,
[soft_block_indent(&format_with(|f| {
// TODO(charlie): If the magic trailing comma isn't present, and the
// tuple is _already_ expanded, we're not supposed to add this.
let magic_trailing_comma = expr
.trivia
.iter()
@ -641,10 +640,21 @@ fn format_joined_str(
fn format_constant(
f: &mut Formatter<ASTFormatContext<'_>>,
expr: &Expr,
_constant: &Constant,
constant: &Constant,
_kind: Option<&str>,
) -> FormatResult<()> {
write!(f, [literal(Range::from_located(expr))])?;
match constant {
Constant::None => write!(f, [text("None")])?,
Constant::Bool(value) => {
if *value {
write!(f, [text("True")])?;
} else {
write!(f, [text("False")])?;
}
}
Constant::Str(_) => write!(f, [string_literal(expr)])?,
_ => write!(f, [literal(Range::from_located(expr))])?,
}
Ok(())
}

View file

@ -10,5 +10,6 @@ mod expr;
mod helpers;
mod operator;
mod stmt;
mod strings;
mod unaryop;
mod withitem;

View file

@ -0,0 +1,240 @@
use rustpython_parser::{Mode, Tok};
use ruff_formatter::prelude::*;
use ruff_formatter::{write, Format};
use ruff_text_size::TextSize;
use crate::context::ASTFormatContext;
use crate::core::helpers::{leading_quote, trailing_quote};
use crate::core::types::Range;
use crate::cst::Expr;
use crate::trivia::Parenthesize;
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct StringLiteralPart {
range: Range,
}
impl Format<ASTFormatContext<'_>> for StringLiteralPart {
fn fmt(&self, f: &mut Formatter<ASTFormatContext<'_>>) -> FormatResult<()> {
let (source, start, end) = f.context().locator().slice(self.range);
// Extract leading and trailing quotes.
let content = &source[start..end];
let leading_quote = leading_quote(content).unwrap();
let trailing_quote = trailing_quote(content).unwrap();
let body = &content[leading_quote.len()..content.len() - trailing_quote.len()];
// Determine the correct quote style.
// TODO(charlie): Make this parameterizable.
let mut squotes: usize = 0;
let mut dquotes: usize = 0;
for char in body.chars() {
if char == '\'' {
squotes += 1;
} else if char == '"' {
dquotes += 1;
}
}
let mut is_raw = false;
if leading_quote.contains('r') {
is_raw = true;
f.write_element(FormatElement::StaticText { text: "r" })?;
} else if leading_quote.contains('R') {
is_raw = true;
f.write_element(FormatElement::StaticText { text: "R" })?;
}
if trailing_quote.len() == 1 {
// Single-quoted string.
if dquotes == 0 || squotes > 0 {
// If the body doesn't contain any double quotes, or it contains both single and
// double quotes, use double quotes.
f.write_element(FormatElement::StaticText { text: "\"" })?;
f.write_element(FormatElement::DynamicText {
text: if is_raw {
body.into()
} else {
double_escape(body).into()
},
source_position: TextSize::default(),
})?;
f.write_element(FormatElement::StaticText { text: "\"" })?;
Ok(())
} else {
f.write_element(FormatElement::StaticText { text: "'" })?;
f.write_element(FormatElement::DynamicText {
text: if is_raw {
body.into()
} else {
single_escape(body).into()
},
source_position: TextSize::default(),
})?;
f.write_element(FormatElement::StaticText { text: "'" })?;
Ok(())
}
} else if trailing_quote.len() == 3 {
// Triple-quoted string.
if body.starts_with("\"\"\"") || body.ends_with('"') {
// We only need to use single quotes if the string body starts with three or more
// double quotes, or ends with a double quote. Converting to double quotes in those
// cases would cause a syntax error.
f.write_element(FormatElement::StaticText { text: "'''" })?;
f.write_element(FormatElement::DynamicText {
text: body.to_string().into_boxed_str(),
source_position: TextSize::default(),
})?;
f.write_element(FormatElement::StaticText { text: "'''" })?;
Ok(())
} else {
f.write_element(FormatElement::StaticText { text: "\"\"\"" })?;
f.write_element(FormatElement::DynamicText {
text: body.to_string().into_boxed_str(),
source_position: TextSize::default(),
})?;
f.write_element(FormatElement::StaticText { text: "\"\"\"" })?;
Ok(())
}
} else {
unreachable!("Invalid quote length: {}", trailing_quote.len());
}
}
}
#[inline]
pub const fn string_literal_part(range: Range) -> StringLiteralPart {
StringLiteralPart { range }
}
#[derive(Debug, Copy, Clone)]
pub struct StringLiteral<'a> {
expr: &'a Expr,
}
impl Format<ASTFormatContext<'_>> for StringLiteral<'_> {
fn fmt(&self, f: &mut Formatter<ASTFormatContext<'_>>) -> FormatResult<()> {
let expr = self.expr;
// TODO(charlie): This tokenization needs to happen earlier, so that we can attach
// comments to individual string literals.
let (source, start, end) = f.context().locator().slice(Range::from_located(expr));
let elts =
rustpython_parser::lexer::lex_located(&source[start..end], Mode::Module, expr.location)
.flatten()
.filter_map(|(start, tok, end)| {
if matches!(tok, Tok::String { .. }) {
Some(Range::new(start, end))
} else {
None
}
})
.collect::<Vec<_>>();
write!(
f,
[group(&format_with(|f| {
if matches!(expr.parentheses, Parenthesize::IfExpanded) {
write!(f, [if_group_breaks(&text("("))])?;
}
for (i, elt) in elts.iter().enumerate() {
write!(f, [string_literal_part(*elt)])?;
if i < elts.len() - 1 {
write!(f, [soft_line_break_or_space()])?;
}
}
if matches!(expr.parentheses, Parenthesize::IfExpanded) {
write!(f, [if_group_breaks(&text(")"))])?;
}
Ok(())
}))]
)?;
Ok(())
}
}
#[inline]
pub const fn string_literal(expr: &Expr) -> StringLiteral {
StringLiteral { expr }
}
/// Escape a string body to be used in a string literal with double quotes.
fn double_escape(text: &str) -> String {
let mut escaped = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
let Some(next) = chars.peek() else {
break;
};
if *next == '\'' {
chars.next();
escaped.push('\'');
} else if *next == '"' {
chars.next();
escaped.push('"');
} else if *next == '\\' {
chars.next();
escaped.push('\\');
escaped.push(ch);
} else {
escaped.push(ch);
}
} else if ch == '"' {
escaped.push('\\');
escaped.push('"');
} else {
escaped.push(ch);
}
}
escaped
}
/// Escape a string body to be used in a string literal with single quotes.
fn single_escape(text: &str) -> String {
let mut escaped = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
let Some(next) = chars.peek() else {
break;
};
if *next == '"' {
chars.next();
escaped.push('"');
} else if *next == '\'' {
chars.next();
escaped.push('\'');
} else if *next == '\\' {
chars.next();
escaped.push('\\');
escaped.push(ch);
} else {
escaped.push(ch);
}
} else if ch == '\'' {
escaped.push('\\');
escaped.push('\'');
} else {
escaped.push(ch);
}
}
escaped
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_double_escape() {
assert_eq!(double_escape(r#"It\'s mine"#), r#"It's mine"#);
assert_eq!(double_escape(r#"It\'s "mine""#), r#"It's \"mine\""#);
assert_eq!(double_escape(r#"It\\'s mine"#), r#"It\\'s mine"#);
}
#[test]
fn test_single_escape() {
assert_eq!(single_escape(r#"It's \"mine\""#), r#"It\'s "mine""#);
}
}

View file

@ -1,49 +0,0 @@
---
source: crates/ruff_python_formatter/src/lib.rs
expression: snapshot
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/bracketmatch.py
---
## Input
```py
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
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -1,4 +1,4 @@
-for ((x in {}) or {})["a"] in x:
+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
+lambda x=lambda y={1: 3}: y['x' : lambda y: {1: 2}]: x
```
## Ruff Output
```py
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
```
## Black Output
```py
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
```

View file

@ -1,319 +0,0 @@
---
source: crates/ruff_python_formatter/src/lib.rs
expression: snapshot
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/collections.py
---
## Input
```py
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,},
)
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -47,7 +47,7 @@
"oneple": (1,),
}
{"oneple": (1,)}
-["ls", "lsoneple/%s" % (foo,)]
+['ls', 'lsoneple/%s' % (foo,)]
x = {"oneple": (1,)}
y = {
"oneple": (1,),
@@ -79,10 +79,10 @@
)
if True:
- ec2client.get_waiter("instance_stopped").wait(
+ ec2client.get_waiter('instance_stopped').wait(
InstanceIds=[instance.id],
WaiterConfig={
- "Delay": 5,
+ 'Delay': 5,
},
)
ec2client.get_waiter("instance_stopped").wait(
```
## Ruff Output
```py
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,
},
)
```
## Black Output
```py
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,
},
)
```

View file

@ -178,7 +178,7 @@ instruction()#comment with bad spacing
```diff
--- Black
+++ Ruff
@@ -1,31 +1,31 @@
@@ -1,8 +1,8 @@
from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import (
- MyLovelyCompanyTeamProjectComponent, # NOT DRY
+ MyLovelyCompanyTeamProjectComponent,
@ -189,52 +189,25 @@ instruction()#comment with bad spacing
)
# Please keep __all__ alphabetized within each category.
__all__ = [
# Super-special typing primitives.
- "Any",
- "Callable",
- "ClassVar",
+ 'Any',
+ 'Callable',
+ 'ClassVar',
@@ -13,7 +13,7 @@
"Callable",
"ClassVar",
# ABCs (from collections.abc).
- "AbstractSet", # collections.abc.Set.
- "ByteString",
- "Container",
+ 'AbstractSet',
+ 'ByteString',
+ 'Container',
+ "AbstractSet",
"ByteString",
"Container",
# Concrete collection types.
- "Counter",
- "Deque",
- "Dict",
- "DefaultDict",
- "List",
- "Set",
- "FrozenSet",
@@ -24,7 +24,7 @@
"List",
"Set",
"FrozenSet",
- "NamedTuple", # Not really a type.
- "Generator",
+ 'Counter',
+ 'Deque',
+ 'Dict',
+ 'DefaultDict',
+ 'List',
+ 'Set',
+ 'FrozenSet',
+ 'NamedTuple',
+ 'Generator',
+ "NamedTuple",
"Generator",
]
not_shareables = [
@@ -48,38 +48,45 @@
SubBytes(b"spam"),
]
-if "PYTHON" in os.environ:
+if 'PYTHON' in os.environ:
add_compiler(compiler_from_env())
else:
@@ -54,32 +54,39 @@
# for compiler in compilers.values():
# add_compiler(compiler)
add_compiler(compilers[(7.0, 32)])
@ -284,7 +257,7 @@ instruction()#comment with bad spacing
):
pass
# no newline before or after
@@ -103,47 +110,47 @@
@@ -103,42 +110,42 @@
############################################################################
call2(
@ -323,12 +296,11 @@ instruction()#comment with bad spacing
]
lcomp3 = [
# This one is actually too long to fit in a single line.
- element.split("\n", 1)[0]
element.split("\n", 1)[0]
- # yup
- for element in collection.select_elements()
- # right
- if element is not None
+ element.split('\n', 1)[0]
+ for # yup
+ element in collection.select_elements()
+ if # right
@ -346,12 +318,6 @@ instruction()#comment with bad spacing
# let's return
return Node(
syms.simple_stmt,
- [Node(statement, result), Leaf(token.NEWLINE, "\n")], # FIXME: \r\n?
+ [Node(statement, result), Leaf(token.NEWLINE, '\n')], # FIXME: \r\n?
)
@@ -167,7 +174,7 @@
#######################
@ -377,23 +343,23 @@ from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component
__all__ = [
# Super-special typing primitives.
'Any',
'Callable',
'ClassVar',
"Any",
"Callable",
"ClassVar",
# ABCs (from collections.abc).
'AbstractSet',
'ByteString',
'Container',
"AbstractSet",
"ByteString",
"Container",
# Concrete collection types.
'Counter',
'Deque',
'Dict',
'DefaultDict',
'List',
'Set',
'FrozenSet',
'NamedTuple',
'Generator',
"Counter",
"Deque",
"Dict",
"DefaultDict",
"List",
"Set",
"FrozenSet",
"NamedTuple",
"Generator",
]
not_shareables = [
@ -416,7 +382,7 @@ not_shareables = [
SubBytes(b"spam"),
]
if 'PYTHON' in os.environ:
if "PYTHON" in os.environ:
add_compiler(compiler_from_env())
else:
# for compiler in compilers.values():
@ -501,7 +467,7 @@ short
]
lcomp3 = [
# This one is actually too long to fit in a single line.
element.split('\n', 1)[0]
element.split("\n", 1)[0]
for # yup
element in collection.select_elements()
if # right
@ -518,7 +484,7 @@ short
# let's return
return Node(
syms.simple_stmt,
[Node(statement, result), Leaf(token.NEWLINE, '\n')], # FIXME: \r\n?
[Node(statement, result), Leaf(token.NEWLINE, "\n")], # FIXME: \r\n?
)

View file

@ -105,20 +105,7 @@ def g():
```diff
--- Black
+++ Ruff
@@ -3,9 +3,9 @@
# leading comment
def f():
- NO = ""
- SPACE = " "
- DOUBLESPACE = " "
+ NO = ''
+ SPACE = ' '
+ DOUBLESPACE = ' '
t = leaf.type
p = leaf.parent # trailing comment
@@ -25,35 +25,41 @@
@@ -25,23 +25,30 @@
return NO
if prevp.type == token.EQUAL:
@ -164,21 +151,14 @@ def g():
return NO
###############################################################################
@@ -49,7 +56,6 @@
# SECTION BECAUSE SECTIONS
###############################################################################
-
def g():
- NO = ""
- SPACE = " "
- DOUBLESPACE = " "
+ NO = ''
+ SPACE = ' '
+ DOUBLESPACE = ' '
t = leaf.type
p = leaf.parent
NO = ""
SPACE = " "
@@ -67,7 +73,7 @@
return DOUBLESPACE
@ -221,9 +201,9 @@ def g():
# leading comment
def f():
NO = ''
SPACE = ' '
DOUBLESPACE = ' '
NO = ""
SPACE = " "
DOUBLESPACE = " "
t = leaf.type
p = leaf.parent # trailing comment
@ -275,9 +255,9 @@ def f():
###############################################################################
def g():
NO = ''
SPACE = ' '
DOUBLESPACE = ' '
NO = ""
SPACE = " "
DOUBLESPACE = " "
t = leaf.type
p = leaf.parent

View file

@ -268,22 +268,16 @@ last_call()
--- Black
+++ Ruff
@@ -1,5 +1,6 @@
-"some_string"
-b"\\xa3"
+...
+'some_string'
"some_string"
-b"\\xa3"
+b'\\xa3'
Name
None
True
@@ -35,10 +36,11 @@
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
+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
@@ -38,7 +39,8 @@
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: {
+foo = lambda port_id,
@ -291,38 +285,16 @@ last_call()
"port1": port1_resource,
"port2": port2_resource,
}[port_id]
@@ -52,11 +54,11 @@
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}}
+{'2.7': dead, '3.7': (long_live or die_hard)}
+{'2.7': dead, '3.7': (long_live or die_hard), **{'3.6': verygood}}
@@ -56,7 +58,7 @@
{"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")}
{"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
+{'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
+({"a": "b"}, (True or False), (+value), "string", b'bytes') or None
()
(1,)
(1, 2)
@@ -88,32 +90,33 @@
]
{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) 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) 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"))}
+{i: j for i, j in ((1, 'a'), (2, 'b'), (3, 'c'))}
{a: b * 2 for a, b in dictionary.items()}
@@ -100,7 +102,8 @@
{a: b * -2 for a, b in dictionary.items()}
{
k: v
@ -332,23 +304,6 @@ last_call()
}
Python3 > Python2 > COBOL
Life is Life
call()
call(arg)
-call(kwarg="hey")
-call(arg, kwarg="hey")
-call(arg, another, kwarg="hey", **kwargs)
+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",
+ kwarg='hey',
**kwargs,
) # note: no trailing comma pre-3.6
call(*gidgets[:2])
@@ -122,8 +125,8 @@
call(b, **self.screen_kwargs)
lukasz.langa.pl
@ -367,19 +322,19 @@ last_call()
-xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore
- sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)
+xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = (
+ classmethod(sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)) # type: ignore
+)
+xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = (
+ classmethod(sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)) # type: ignore
)
-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
+ classmethod(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
+xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = (
+ classmethod(sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__))
+)
slice[0]
slice[0:1]
slice[0:1:2]
@ -405,46 +360,25 @@ last_call()
numpy[:, (0, 1, 2, 5)]
numpy[0, [0]]
numpy[:, [i]]
@@ -172,17 +175,17 @@
@@ -172,7 +175,7 @@
numpy[-(c + 1) :, d]
numpy[:, l[-2]]
numpy[:, ::-1]
-numpy[np.newaxis, :]
+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"}
+{'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) 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,)
{
{"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"}
@@ -201,30 +204,26 @@
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)
)
-)
-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(
@ -452,7 +386,11 @@ last_call()
- )
- .order_by(models.Customer.id.asc())
- .all()
-)
+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(
@ -462,7 +400,11 @@ last_call()
- models.Customer.id.asc(),
- )
- .all()
-)
+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,
@ -489,15 +431,6 @@ last_call()
assert parens is TooMany
for (x,) in (1,), (2,), (3,):
...
@@ -272,7 +271,7 @@
addr_proto,
addr_canonname,
addr_sockaddr,
-) in socket.getaddrinfo("google.com", "http"):
+) in socket.getaddrinfo('google.com', 'http'):
pass
a = (
aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp
@@ -327,13 +326,18 @@
):
return True
@ -536,7 +469,7 @@ last_call()
```py
...
'some_string'
"some_string"
b'\\xa3'
Name
None
@ -573,8 +506,8 @@ 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
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,: {
@ -591,11 +524,11 @@ str or None if (1 if True else 2) else str or bytes 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}}
{"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
{"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)
@ -627,14 +560,14 @@ str or None if (1 if True else 2) else str or bytes or None
]
{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) 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) 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'))}
{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()}
{
@ -646,14 +579,14 @@ Python3 > Python2 > COBOL
Life is Life
call()
call(arg)
call(kwarg='hey')
call(arg, kwarg='hey')
call(arg, another, kwarg='hey', **kwargs)
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',
kwarg="hey",
**kwargs,
) # note: no trailing comma pre-3.6
call(*gidgets[:2])
@ -714,15 +647,15 @@ 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'}
{"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) 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,)
{
@ -808,7 +741,7 @@ for (
addr_proto,
addr_canonname,
addr_sockaddr,
) in socket.getaddrinfo('google.com', 'http'):
) in socket.getaddrinfo("google.com", "http"):
pass
a = (
aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp

View file

@ -67,11 +67,11 @@ def test_calculate_fades():
-
- # Test don't manage the volume
+@pytest.mark.parametrize(
+ 'test',
+ "test",
[
- ('stuff', 'in')
+ # Test don't manage the volume
+ [('stuff', 'in')],
+ [("stuff", "in")],
],
-])
+)
@ -123,10 +123,10 @@ TmEx = 2
# Position, Volume, State, TmSt/TmEx/None, [call, [arg1...]]
@pytest.mark.parametrize(
'test',
"test",
[
# Test don't manage the volume
[('stuff', 'in')],
[("stuff", "in")],
],
)
def test_fader(test):

View file

@ -97,17 +97,20 @@ elif unformatted:
```diff
--- Black
+++ Ruff
@@ -5,8 +5,7 @@
@@ -3,10 +3,8 @@
entry_points={
# fmt: off
"console_scripts": [
"foo-bar"
"=foo.bar.:main",
- "foo-bar"
- "=foo.bar.:main",
- # fmt: on
- ] # Includes an formatted indentation.
+ "foo-bar" "=foo.bar.:main",
+ ],
},
)
@@ -18,8 +17,8 @@
@@ -18,8 +16,8 @@
"ls",
"-la",
]
@ -118,7 +121,7 @@ elif unformatted:
check=True,
)
@@ -27,9 +26,8 @@
@@ -27,9 +25,8 @@
# Regression test for https://github.com/psf/black/issues/3026.
def test_func():
# yapf: disable
@ -129,7 +132,7 @@ elif unformatted:
elif b:
return True
@@ -39,10 +37,10 @@
@@ -39,10 +36,10 @@
# Regression test for https://github.com/psf/black/issues/2567.
if True:
# fmt: off
@ -144,7 +147,7 @@ elif unformatted:
else:
print("This will be formatted")
@@ -52,14 +50,11 @@
@@ -52,14 +49,11 @@
async def call(param):
if param:
# fmt: off
@ -162,7 +165,7 @@ elif unformatted:
print("This will be formatted")
@@ -68,20 +63,21 @@
@@ -68,20 +62,21 @@
class Named(t.Protocol):
# fmt: off
@property
@ -198,8 +201,7 @@ setup(
entry_points={
# fmt: off
"console_scripts": [
"foo-bar"
"=foo.bar.:main",
"foo-bar" "=foo.bar.:main",
],
},
)

View file

@ -117,32 +117,7 @@ def __await__(): return (yield)
def func_no_args():
@@ -27,7 +27,7 @@
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 conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2)
await asyncio.sleep(1)
@@ -44,7 +44,7 @@
return text[number:-1]
-def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""):
+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
@@ -58,25 +58,20 @@
f: int = -1,
g: int = 1 if False else 2,
h: str = "",
- i: str = r"",
+ i: str = r'',
):
...
@@ -64,19 +64,14 @@
def spaces2(result=_core.Value(None)):
@ -218,7 +193,7 @@ def func_no_args():
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 conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2)
await asyncio.sleep(1)
@ -235,7 +210,7 @@ def function_signature_stress_test(
return text[number:-1]
def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''):
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
@ -249,7 +224,7 @@ def spaces_types(
f: int = -1,
g: int = 1 if False else 2,
h: str = "",
i: str = r'',
i: str = r"",
):
...

View file

@ -74,35 +74,6 @@ some_module.some_function(
```diff
--- Black
+++ Ruff
@@ -2,7 +2,7 @@
a,
):
d = {
- "key": "value",
+ 'key': 'value',
}
tup = (1,)
@@ -12,8 +12,8 @@
b,
):
d = {
- "key": "value",
- "key2": "value2",
+ 'key': 'value',
+ 'key2': 'value2',
}
tup = (
1,
@@ -26,7 +26,7 @@
):
call(
arg={
- "explode": "this",
+ 'explode': 'this',
}
)
call2(
@@ -52,53 +52,52 @@
pass
@ -184,7 +155,7 @@ def f(
a,
):
d = {
'key': 'value',
"key": "value",
}
tup = (1,)
@ -194,8 +165,8 @@ def f2(
b,
):
d = {
'key': 'value',
'key2': 'value2',
"key": "value",
"key2": "value2",
}
tup = (
1,
@ -208,7 +179,7 @@ def f(
):
call(
arg={
'explode': 'this',
"explode": "this",
}
)
call2(

View file

@ -39,10 +39,9 @@ def docstring_multiline():
name = "Łukasz"
-(f"hello {name}", f"hello {name}")
-(b"", b"")
-("", "")
+(f"hello {name}", F"hello {name}")
+(b"", B"")
+(u"", U"")
("", "")
(r"", R"")
-(rf"", rf"", Rf"", Rf"", rf"", rf"", Rf"", Rf"")
@ -62,7 +61,7 @@ def docstring_multiline():
name = "Łukasz"
(f"hello {name}", F"hello {name}")
(b"", B"")
(u"", U"")
("", "")
(r"", R"")
(rf"", fr"", Rf"", fR"", rF"", Fr"", RF"", FR"")

View file

@ -38,7 +38,7 @@ class A:
```diff
--- Black
+++ Ruff
@@ -1,15 +1,14 @@
@@ -1,7 +1,6 @@
-if e1234123412341234.winerror not in (
- _winapi.ERROR_SEM_TIMEOUT,
- _winapi.ERROR_PIPE_BUSY,
@ -49,23 +49,13 @@ class A:
pass
if x:
if y:
new_id = (
max(
- Vegetable.objects.order_by("-id")[0].id,
- Mineral.objects.order_by("-id")[0].id,
+ Vegetable.objects.order_by('-id')[0].id,
+ Mineral.objects.order_by('-id')[0].id,
)
+ 1
)
@@ -21,14 +20,20 @@
"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}
+ )
+ % {'min_length': self.min_length}
+ % {"min_length": self.min_length}
class A:
@ -100,8 +90,8 @@ if x:
if y:
new_id = (
max(
Vegetable.objects.order_by('-id')[0].id,
Mineral.objects.order_by('-id')[0].id,
Vegetable.objects.order_by("-id")[0].id,
Mineral.objects.order_by("-id")[0].id,
)
+ 1
)
@ -114,7 +104,7 @@ class X:
"Your password must contain at least %(min_length)d characters.",
self.min_length,
)
% {'min_length': self.min_length}
% {"min_length": self.min_length}
class A:

View file

@ -23,7 +23,7 @@ if (e123456.get_tk_patchlevel() >= (8, 6, 0, 'final') or
- 8,
-) <= get_tk_patchlevel() < (8, 6):
+if (
+ e123456.get_tk_patchlevel() >= (8, 6, 0, 'final')
+ e123456.get_tk_patchlevel() >= (8, 6, 0, "final")
+ or (8, 5, 8) <= get_tk_patchlevel() < (8, 6)
+):
pass
@ -33,7 +33,7 @@ if (e123456.get_tk_patchlevel() >= (8, 6, 0, 'final') or
```py
if (
e123456.get_tk_patchlevel() >= (8, 6, 0, 'final')
e123456.get_tk_patchlevel() >= (8, 6, 0, "final")
or (8, 5, 8) <= get_tk_patchlevel() < (8, 6)
):
pass