44
44
from blib2to3 .pgen2 .parse import ParseError
45
45
46
46
47
- __version__ = "18.5b1 "
47
+ __version__ = "18.5z1 "
48
48
DEFAULT_LINE_LENGTH = 88
49
49
CACHE_DIR = Path (user_cache_dir ("black" , version = __version__ ))
50
50
@@ -126,6 +126,7 @@ class FileMode(Flag):
126
126
AUTO_DETECT = 0
127
127
PYTHON36 = 1
128
128
PYI = 2
129
+ SINGLE_QUOTE = 32
129
130
130
131
131
132
@click .command ()
@@ -137,6 +138,11 @@ class FileMode(Flag):
137
138
help = "How many character per line to allow." ,
138
139
show_default = True ,
139
140
)
141
+ @click .option (
142
+ "--single-quote" ,
143
+ is_flag = True ,
144
+ help = "Use single quotes instead of double quotes in strings." ,
145
+ )
140
146
@click .option (
141
147
"--check" ,
142
148
is_flag = True ,
@@ -195,6 +201,7 @@ def main(
195
201
ctx : click .Context ,
196
202
line_length : int ,
197
203
check : bool ,
204
+ single_quote : bool ,
198
205
diff : bool ,
199
206
fast : bool ,
200
207
pyi : bool ,
@@ -227,6 +234,8 @@ def main(
227
234
mode |= FileMode .PYTHON36
228
235
if pyi :
229
236
mode |= FileMode .PYI
237
+ if single_quote :
238
+ mode |= FileMode .SINGLE_QUOTE
230
239
report = Report (check = check , quiet = quiet )
231
240
if len (sources ) == 0 :
232
241
out ("No paths given. Nothing to do 😴" )
@@ -487,8 +496,11 @@ def format_str(
487
496
future_imports = get_future_imports (src_node )
488
497
is_pyi = bool (mode & FileMode .PYI )
489
498
py36 = bool (mode & FileMode .PYTHON36 ) or is_python36 (src_node )
499
+ single_quote = bool (mode & FileMode .SINGLE_QUOTE )
490
500
lines = LineGenerator (
491
- remove_u_prefix = py36 or "unicode_literals" in future_imports , is_pyi = is_pyi
501
+ remove_u_prefix = py36 or "unicode_literals" in future_imports ,
502
+ is_pyi = is_pyi ,
503
+ single_quote = single_quote ,
492
504
)
493
505
elt = EmptyLineTracker (is_pyi = is_pyi )
494
506
empty_line = Line ()
@@ -1286,6 +1298,7 @@ class LineGenerator(Visitor[Line]):
1286
1298
"""
1287
1299
1288
1300
is_pyi : bool = False
1301
+ single_quote : bool = False
1289
1302
current_line : Line = Factory (Line )
1290
1303
remove_u_prefix : bool = False
1291
1304
@@ -1356,7 +1369,7 @@ def visit_default(self, node: LN) -> Iterator[Line]:
1356
1369
normalize_prefix (node , inside_brackets = any_open_brackets )
1357
1370
if node .type == token .STRING :
1358
1371
normalize_string_prefix (node , remove_u_prefix = self .remove_u_prefix )
1359
- normalize_string_quotes (node )
1372
+ normalize_string_quotes (node , single_quote = self . single_quote )
1360
1373
if node .type not in WHITESPACE :
1361
1374
self .current_line .append (node )
1362
1375
yield from super ().visit_default (node )
@@ -2313,7 +2326,7 @@ def normalize_string_prefix(leaf: Leaf, remove_u_prefix: bool = False) -> None:
2313
2326
leaf .value = f"{ new_prefix } { match .group (2 )} "
2314
2327
2315
2328
2316
- def normalize_string_quotes (leaf : Leaf ) -> None :
2329
+ def normalize_string_quotes (leaf : Leaf , single_quote : bool = False ) -> None :
2317
2330
"""Prefer double quotes but only if it doesn't cause more escaping.
2318
2331
2319
2332
Adds or removes backslashes as appropriate. Doesn't parse and fix
@@ -2322,18 +2335,28 @@ def normalize_string_quotes(leaf: Leaf) -> None:
2322
2335
Note: Mutates its argument.
2323
2336
"""
2324
2337
value = leaf .value .lstrip ("furbFURB" )
2325
- if value [:3 ] == '"""' :
2338
+
2339
+ quote_char = '"'
2340
+ alt_quote_char = "'"
2341
+ triple_quote_chars = '"""'
2342
+ alt_triple_quote_chars = "'''"
2343
+
2344
+ if single_quote :
2345
+ quote_char = "'"
2346
+ alt_quote_char = '"'
2347
+
2348
+ if value [:3 ] == triple_quote_chars :
2326
2349
return
2327
2350
2328
- elif value [:3 ] == "'''" :
2329
- orig_quote = "'''"
2330
- new_quote = '"""'
2331
- elif value [0 ] == '"' :
2332
- orig_quote = '"'
2333
- new_quote = "'"
2351
+ elif value [:3 ] == alt_triple_quote_chars :
2352
+ orig_quote = alt_triple_quote_chars
2353
+ new_quote = triple_quote_chars
2354
+ elif value [0 ] == quote_char :
2355
+ orig_quote = quote_char
2356
+ new_quote = alt_quote_char
2334
2357
else :
2335
- orig_quote = "'"
2336
- new_quote = '"'
2358
+ orig_quote = alt_quote_char
2359
+ new_quote = quote_char
2337
2360
first_quote_pos = leaf .value .find (orig_quote )
2338
2361
if first_quote_pos == - 1 :
2339
2362
return # There's an internal error
@@ -2360,16 +2383,16 @@ def normalize_string_quotes(leaf: Leaf) -> None:
2360
2383
leaf .value = f"{ prefix } { orig_quote } { body } { orig_quote } "
2361
2384
new_body = sub_twice (escaped_orig_quote , rf"\1\2{ orig_quote } " , new_body )
2362
2385
new_body = sub_twice (unescaped_new_quote , rf"\1\\{ new_quote } " , new_body )
2363
- if new_quote == '"""' and new_body [- 1 ] == '"' :
2386
+ if new_quote == triple_quote_chars and new_body [- 1 ] == triple_quote_chars [ 0 ] :
2364
2387
# edge case:
2365
- new_body = new_body [:- 1 ] + ' \\ "'
2388
+ new_body = new_body [:- 1 ] + " \\ " + triple_quote_chars [ 0 ]
2366
2389
orig_escape_count = body .count ("\\ " )
2367
2390
new_escape_count = new_body .count ("\\ " )
2368
2391
if new_escape_count > orig_escape_count :
2369
2392
return # Do not introduce more escaping
2370
2393
2371
- if new_escape_count == orig_escape_count and orig_quote == '"' :
2372
- return # Prefer double quotes
2394
+ if new_escape_count == orig_escape_count and orig_quote == quote_char :
2395
+ return
2373
2396
2374
2397
leaf .value = f"{ prefix } { new_quote } { new_body } { new_quote } "
2375
2398
0 commit comments