13
13
from pathlib import Path
14
14
from typing import (
15
15
Any ,
16
+ Collection ,
16
17
Dict ,
17
18
Generator ,
18
19
Iterator ,
77
78
from black .output import color_diff , diff , dump_to_file , err , ipynb_diff , out
78
79
from black .parsing import InvalidInput # noqa F401
79
80
from black .parsing import lib2to3_parse , parse_ast , stringify_ast
81
+ from black .ranges import adjusted_lines , convert_unchanged_lines , parse_line_ranges
80
82
from black .report import Changed , NothingChanged , Report
81
83
from black .trans import iter_fexpr_spans
82
84
from blib2to3 .pgen2 import token
@@ -163,6 +165,12 @@ def read_pyproject_toml(
163
165
"extend-exclude" , "Config key extend-exclude must be a string"
164
166
)
165
167
168
+ line_ranges = config .get ("line_ranges" )
169
+ if line_ranges is not None :
170
+ raise click .BadOptionUsage (
171
+ "line-ranges" , "Cannot use line-ranges in the pyproject.toml file."
172
+ )
173
+
166
174
default_map : Dict [str , Any ] = {}
167
175
if ctx .default_map :
168
176
default_map .update (ctx .default_map )
@@ -304,6 +312,19 @@ def validate_regex(
304
312
is_flag = True ,
305
313
help = "Don't write the files back, just output a diff for each file on stdout." ,
306
314
)
315
+ @click .option (
316
+ "--line-ranges" ,
317
+ multiple = True ,
318
+ metavar = "START-END" ,
319
+ help = (
320
+ "When specified, _Black_ will try its best to only format these lines. This"
321
+ " option can be specified multiple times, and a union of the lines will be"
322
+ " formatted. Each range must be specified as two integers connected by a `-`:"
323
+ " `<START>-<END>`. The `<START>` and `<END>` integer indices are 1-based and"
324
+ " inclusive on both ends."
325
+ ),
326
+ default = (),
327
+ )
307
328
@click .option (
308
329
"--color/--no-color" ,
309
330
is_flag = True ,
@@ -443,6 +464,7 @@ def main( # noqa: C901
443
464
target_version : List [TargetVersion ],
444
465
check : bool ,
445
466
diff : bool ,
467
+ line_ranges : Sequence [str ],
446
468
color : bool ,
447
469
fast : bool ,
448
470
pyi : bool ,
@@ -544,6 +566,18 @@ def main( # noqa: C901
544
566
python_cell_magics = set (python_cell_magics ),
545
567
)
546
568
569
+ lines : List [Tuple [int , int ]] = []
570
+ if line_ranges :
571
+ if ipynb :
572
+ err ("Cannot use --line-ranges with ipynb files." )
573
+ ctx .exit (1 )
574
+
575
+ try :
576
+ lines = parse_line_ranges (line_ranges )
577
+ except ValueError as e :
578
+ err (str (e ))
579
+ ctx .exit (1 )
580
+
547
581
if code is not None :
548
582
# Run in quiet mode by default with -c; the extra output isn't useful.
549
583
# You can still pass -v to get verbose output.
@@ -553,7 +587,12 @@ def main( # noqa: C901
553
587
554
588
if code is not None :
555
589
reformat_code (
556
- content = code , fast = fast , write_back = write_back , mode = mode , report = report
590
+ content = code ,
591
+ fast = fast ,
592
+ write_back = write_back ,
593
+ mode = mode ,
594
+ report = report ,
595
+ lines = lines ,
557
596
)
558
597
else :
559
598
assert root is not None # root is only None if code is not None
@@ -588,10 +627,14 @@ def main( # noqa: C901
588
627
write_back = write_back ,
589
628
mode = mode ,
590
629
report = report ,
630
+ lines = lines ,
591
631
)
592
632
else :
593
633
from black .concurrency import reformat_many
594
634
635
+ if lines :
636
+ err ("Cannot use --line-ranges to format multiple files." )
637
+ ctx .exit (1 )
595
638
reformat_many (
596
639
sources = sources ,
597
640
fast = fast ,
@@ -714,7 +757,13 @@ def path_empty(
714
757
715
758
716
759
def reformat_code (
717
- content : str , fast : bool , write_back : WriteBack , mode : Mode , report : Report
760
+ content : str ,
761
+ fast : bool ,
762
+ write_back : WriteBack ,
763
+ mode : Mode ,
764
+ report : Report ,
765
+ * ,
766
+ lines : Collection [Tuple [int , int ]] = (),
718
767
) -> None :
719
768
"""
720
769
Reformat and print out `content` without spawning child processes.
@@ -727,7 +776,7 @@ def reformat_code(
727
776
try :
728
777
changed = Changed .NO
729
778
if format_stdin_to_stdout (
730
- content = content , fast = fast , write_back = write_back , mode = mode
779
+ content = content , fast = fast , write_back = write_back , mode = mode , lines = lines
731
780
):
732
781
changed = Changed .YES
733
782
report .done (path , changed )
@@ -741,7 +790,13 @@ def reformat_code(
741
790
# not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26
742
791
@mypyc_attr (patchable = True )
743
792
def reformat_one (
744
- src : Path , fast : bool , write_back : WriteBack , mode : Mode , report : "Report"
793
+ src : Path ,
794
+ fast : bool ,
795
+ write_back : WriteBack ,
796
+ mode : Mode ,
797
+ report : "Report" ,
798
+ * ,
799
+ lines : Collection [Tuple [int , int ]] = (),
745
800
) -> None :
746
801
"""Reformat a single file under `src` without spawning child processes.
747
802
@@ -766,15 +821,17 @@ def reformat_one(
766
821
mode = replace (mode , is_pyi = True )
767
822
elif src .suffix == ".ipynb" :
768
823
mode = replace (mode , is_ipynb = True )
769
- if format_stdin_to_stdout (fast = fast , write_back = write_back , mode = mode ):
824
+ if format_stdin_to_stdout (
825
+ fast = fast , write_back = write_back , mode = mode , lines = lines
826
+ ):
770
827
changed = Changed .YES
771
828
else :
772
829
cache = Cache .read (mode )
773
830
if write_back not in (WriteBack .DIFF , WriteBack .COLOR_DIFF ):
774
831
if not cache .is_changed (src ):
775
832
changed = Changed .CACHED
776
833
if changed is not Changed .CACHED and format_file_in_place (
777
- src , fast = fast , write_back = write_back , mode = mode
834
+ src , fast = fast , write_back = write_back , mode = mode , lines = lines
778
835
):
779
836
changed = Changed .YES
780
837
if (write_back is WriteBack .YES and changed is not Changed .CACHED ) or (
@@ -794,6 +851,8 @@ def format_file_in_place(
794
851
mode : Mode ,
795
852
write_back : WriteBack = WriteBack .NO ,
796
853
lock : Any = None , # multiprocessing.Manager().Lock() is some crazy proxy
854
+ * ,
855
+ lines : Collection [Tuple [int , int ]] = (),
797
856
) -> bool :
798
857
"""Format file under `src` path. Return True if changed.
799
858
@@ -813,7 +872,9 @@ def format_file_in_place(
813
872
header = buf .readline ()
814
873
src_contents , encoding , newline = decode_bytes (buf .read ())
815
874
try :
816
- dst_contents = format_file_contents (src_contents , fast = fast , mode = mode )
875
+ dst_contents = format_file_contents (
876
+ src_contents , fast = fast , mode = mode , lines = lines
877
+ )
817
878
except NothingChanged :
818
879
return False
819
880
except JSONDecodeError :
@@ -858,6 +919,7 @@ def format_stdin_to_stdout(
858
919
content : Optional [str ] = None ,
859
920
write_back : WriteBack = WriteBack .NO ,
860
921
mode : Mode ,
922
+ lines : Collection [Tuple [int , int ]] = (),
861
923
) -> bool :
862
924
"""Format file on stdin. Return True if changed.
863
925
@@ -876,7 +938,7 @@ def format_stdin_to_stdout(
876
938
877
939
dst = src
878
940
try :
879
- dst = format_file_contents (src , fast = fast , mode = mode )
941
+ dst = format_file_contents (src , fast = fast , mode = mode , lines = lines )
880
942
return True
881
943
882
944
except NothingChanged :
@@ -904,7 +966,11 @@ def format_stdin_to_stdout(
904
966
905
967
906
968
def check_stability_and_equivalence (
907
- src_contents : str , dst_contents : str , * , mode : Mode
969
+ src_contents : str ,
970
+ dst_contents : str ,
971
+ * ,
972
+ mode : Mode ,
973
+ lines : Collection [Tuple [int , int ]] = (),
908
974
) -> None :
909
975
"""Perform stability and equivalence checks.
910
976
@@ -913,10 +979,16 @@ def check_stability_and_equivalence(
913
979
content differently.
914
980
"""
915
981
assert_equivalent (src_contents , dst_contents )
916
- assert_stable (src_contents , dst_contents , mode = mode )
982
+ assert_stable (src_contents , dst_contents , mode = mode , lines = lines )
917
983
918
984
919
- def format_file_contents (src_contents : str , * , fast : bool , mode : Mode ) -> FileContent :
985
+ def format_file_contents (
986
+ src_contents : str ,
987
+ * ,
988
+ fast : bool ,
989
+ mode : Mode ,
990
+ lines : Collection [Tuple [int , int ]] = (),
991
+ ) -> FileContent :
920
992
"""Reformat contents of a file and return new contents.
921
993
922
994
If `fast` is False, additionally confirm that the reformatted code is
@@ -926,13 +998,15 @@ def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileCo
926
998
if mode .is_ipynb :
927
999
dst_contents = format_ipynb_string (src_contents , fast = fast , mode = mode )
928
1000
else :
929
- dst_contents = format_str (src_contents , mode = mode )
1001
+ dst_contents = format_str (src_contents , mode = mode , lines = lines )
930
1002
if src_contents == dst_contents :
931
1003
raise NothingChanged
932
1004
933
1005
if not fast and not mode .is_ipynb :
934
1006
# Jupyter notebooks will already have been checked above.
935
- check_stability_and_equivalence (src_contents , dst_contents , mode = mode )
1007
+ check_stability_and_equivalence (
1008
+ src_contents , dst_contents , mode = mode , lines = lines
1009
+ )
936
1010
return dst_contents
937
1011
938
1012
@@ -1043,7 +1117,9 @@ def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileCon
1043
1117
raise NothingChanged
1044
1118
1045
1119
1046
- def format_str (src_contents : str , * , mode : Mode ) -> str :
1120
+ def format_str (
1121
+ src_contents : str , * , mode : Mode , lines : Collection [Tuple [int , int ]] = ()
1122
+ ) -> str :
1047
1123
"""Reformat a string and return new contents.
1048
1124
1049
1125
`mode` determines formatting options, such as how many characters per line are
@@ -1073,16 +1149,20 @@ def f(
1073
1149
hey
1074
1150
1075
1151
"""
1076
- dst_contents = _format_str_once (src_contents , mode = mode )
1152
+ dst_contents = _format_str_once (src_contents , mode = mode , lines = lines )
1077
1153
# Forced second pass to work around optional trailing commas (becoming
1078
1154
# forced trailing commas on pass 2) interacting differently with optional
1079
1155
# parentheses. Admittedly ugly.
1080
1156
if src_contents != dst_contents :
1081
- return _format_str_once (dst_contents , mode = mode )
1157
+ if lines :
1158
+ lines = adjusted_lines (lines , src_contents , dst_contents )
1159
+ return _format_str_once (dst_contents , mode = mode , lines = lines )
1082
1160
return dst_contents
1083
1161
1084
1162
1085
- def _format_str_once (src_contents : str , * , mode : Mode ) -> str :
1163
+ def _format_str_once (
1164
+ src_contents : str , * , mode : Mode , lines : Collection [Tuple [int , int ]] = ()
1165
+ ) -> str :
1086
1166
src_node = lib2to3_parse (src_contents .lstrip (), mode .target_versions )
1087
1167
dst_blocks : List [LinesBlock ] = []
1088
1168
if mode .target_versions :
@@ -1097,15 +1177,19 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str:
1097
1177
if supports_feature (versions , feature )
1098
1178
}
1099
1179
normalize_fmt_off (src_node , mode )
1100
- lines = LineGenerator (mode = mode , features = context_manager_features )
1180
+ if lines :
1181
+ # This should be called after normalize_fmt_off.
1182
+ convert_unchanged_lines (src_node , lines )
1183
+
1184
+ line_generator = LineGenerator (mode = mode , features = context_manager_features )
1101
1185
elt = EmptyLineTracker (mode = mode )
1102
1186
split_line_features = {
1103
1187
feature
1104
1188
for feature in {Feature .TRAILING_COMMA_IN_CALL , Feature .TRAILING_COMMA_IN_DEF }
1105
1189
if supports_feature (versions , feature )
1106
1190
}
1107
1191
block : Optional [LinesBlock ] = None
1108
- for current_line in lines .visit (src_node ):
1192
+ for current_line in line_generator .visit (src_node ):
1109
1193
block = elt .maybe_empty_lines (current_line )
1110
1194
dst_blocks .append (block )
1111
1195
for line in transform_line (
@@ -1373,12 +1457,16 @@ def assert_equivalent(src: str, dst: str) -> None:
1373
1457
) from None
1374
1458
1375
1459
1376
- def assert_stable (src : str , dst : str , mode : Mode ) -> None :
1460
+ def assert_stable (
1461
+ src : str , dst : str , mode : Mode , * , lines : Collection [Tuple [int , int ]] = ()
1462
+ ) -> None :
1377
1463
"""Raise AssertionError if `dst` reformats differently the second time."""
1378
1464
# We shouldn't call format_str() here, because that formats the string
1379
1465
# twice and may hide a bug where we bounce back and forth between two
1380
1466
# versions.
1381
- newdst = _format_str_once (dst , mode = mode )
1467
+ if lines :
1468
+ lines = adjusted_lines (lines , src , dst )
1469
+ newdst = _format_str_once (dst , mode = mode , lines = lines )
1382
1470
if dst != newdst :
1383
1471
log = dump_to_file (
1384
1472
str (mode ),
0 commit comments