2525import click
2626from click .core import ParameterSource
2727from mypy_extensions import mypyc_attr
28- from pathspec import PathSpec
29- from pathspec .patterns .gitwildmatch import GitWildMatchPatternError
28+ from pathspec import GitIgnoreSpec
29+ from pathspec .patterns .gitignore import GitIgnorePatternError
3030
3131from _pyink_version import version as __version__
3232from pyink .cache import Cache
@@ -209,6 +209,27 @@ def target_version_option_callback(
209209 return [TargetVersion [val .upper ()] for val in v ]
210210
211211
212+ def _target_versions_exceed_runtime (
213+ target_versions : set [TargetVersion ],
214+ ) -> bool :
215+ if not target_versions :
216+ return False
217+ max_target_minor = max (tv .value for tv in target_versions )
218+ return max_target_minor > sys .version_info [1 ]
219+
220+
221+ def _version_mismatch_message (target_versions : set [TargetVersion ]) -> str :
222+ max_target = max (target_versions , key = lambda tv : tv .value )
223+ runtime = f"{ sys .version_info [0 ]} .{ sys .version_info [1 ]} "
224+ return (
225+ f"Python { runtime } cannot parse code formatted for"
226+ f" { max_target .pretty ()} . To fix this: run Black with"
227+ f" { max_target .pretty ()} , set --target-version to"
228+ f" py3{ sys .version_info [1 ]} , or use --fast to skip the safety"
229+ " check."
230+ )
231+
232+
212233def enable_unstable_feature_callback (
213234 c : click .Context , p : click .Option | click .Parameter , v : tuple [str , ...]
214235) -> list [Preview ]:
@@ -719,6 +740,14 @@ def main(
719740 ),
720741 )
721742
743+ if not fast and _target_versions_exceed_runtime (versions ):
744+ err (
745+ f"Warning: { _version_mismatch_message (versions )} Black's safety"
746+ " check verifies equivalence by parsing the AST, which fails"
747+ " when the running Python is older than the target version." ,
748+ fg = "yellow" ,
749+ )
750+
722751 lines : list [tuple [int , int ]] = []
723752 if line_ranges :
724753 if ipynb :
@@ -762,7 +791,7 @@ def main(
762791 report = report ,
763792 stdin_filename = stdin_filename ,
764793 )
765- except GitWildMatchPatternError :
794+ except GitIgnorePatternError :
766795 ctx .exit (1 )
767796
768797 if not sources :
@@ -826,7 +855,7 @@ def get_sources(
826855 assert root .is_absolute (), f"INTERNAL ERROR: `root` must be absolute but is { root } "
827856 using_default_exclude = exclude is None
828857 exclude = re_compile_maybe_verbose (DEFAULT_EXCLUDES ) if exclude is None else exclude
829- gitignore : dict [Path , PathSpec ] | None = None
858+ gitignore : dict [Path , GitIgnoreSpec ] | None = None
830859 root_gitignore = get_gitignore (root )
831860
832861 for s in src :
@@ -1087,10 +1116,8 @@ def format_stdin_to_stdout(
10871116
10881117 if content is None :
10891118 src , encoding , newline = decode_bytes (sys .stdin .buffer .read (), mode )
1090- elif Preview .normalize_cr_newlines in mode :
1091- src , encoding , newline = content , "utf-8" , "\n "
10921119 else :
1093- src , encoding , newline = content , "utf-8" , ""
1120+ src , encoding , newline = content , "utf-8" , "\n "
10941121
10951122 dst = src
10961123 try :
@@ -1106,12 +1133,8 @@ def format_stdin_to_stdout(
11061133 )
11071134 if write_back == WriteBack .YES :
11081135 # Make sure there's a newline after the content
1109- if Preview .normalize_cr_newlines in mode :
1110- if dst and dst [- 1 ] != "\n " and dst [- 1 ] != "\r " :
1111- dst += newline
1112- else :
1113- if dst and dst [- 1 ] != "\n " :
1114- dst += "\n "
1136+ if dst and dst [- 1 ] != "\n " and dst [- 1 ] != "\r " :
1137+ dst += newline
11151138 f .write (dst )
11161139 elif write_back in (WriteBack .DIFF , WriteBack .COLOR_DIFF ):
11171140 now = datetime .now (timezone .utc )
@@ -1138,7 +1161,15 @@ def check_stability_and_equivalence(
11381161 equivalent, or if a second pass of the formatter would format the
11391162 content differently.
11401163 """
1141- assert_equivalent (src_contents , dst_contents )
1164+ try :
1165+ assert_equivalent (src_contents , dst_contents )
1166+ except ASTSafetyError :
1167+ if _target_versions_exceed_runtime (mode .target_versions ):
1168+ raise ASTSafetyError (
1169+ "failed to verify equivalence of the formatted output:"
1170+ f" { _version_mismatch_message (mode .target_versions )} "
1171+ ) from None
1172+ raise
11421173 assert_stable (src_contents , dst_contents , mode = mode , lines = lines )
11431174
11441175
@@ -1314,16 +1345,15 @@ def f(
13141345def _format_str_once (
13151346 src_contents : str , * , mode : Mode , lines : Collection [tuple [int , int ]] = ()
13161347) -> str :
1317- if Preview .normalize_cr_newlines in mode :
1318- normalized_contents , _ , newline_type = decode_bytes (
1319- src_contents .encode ("utf-8" ), mode
1320- )
1348+ # Use the encoding overwrite since the src_contents may contain a different
1349+ # magic encoding comment than utf-8
1350+ normalized_contents , _ , newline_type = decode_bytes (
1351+ src_contents .encode ("utf-8" ), mode , encoding_overwrite = "utf-8"
1352+ )
13211353
1322- src_node = lib2to3_parse (
1323- normalized_contents .lstrip (), target_versions = mode .target_versions
1324- )
1325- else :
1326- src_node = lib2to3_parse (src_contents .lstrip (), mode .target_versions )
1354+ src_node = lib2to3_parse (
1355+ normalized_contents .lstrip (), target_versions = mode .target_versions
1356+ )
13271357
13281358 dst_blocks : list [LinesBlock ] = []
13291359 if mode .target_versions :
@@ -1372,53 +1402,48 @@ def _format_str_once(
13721402 for block in dst_blocks :
13731403 dst_contents .extend (block .all_lines ())
13741404 if not dst_contents :
1375- if Preview .normalize_cr_newlines in mode :
1376- if "\n " in normalized_contents :
1377- return newline_type
1378- else :
1379- # Use decode_bytes to retrieve the correct source newline (CRLF or LF),
1380- # and check if normalized_content has more than one line
1381- normalized_content , _ , newline = decode_bytes (
1382- src_contents .encode ("utf-8" ), mode
1383- )
1384- if "\n " in normalized_content :
1385- return newline
1386- return ""
1387- if Preview .normalize_cr_newlines in mode :
1388- return "" .join (dst_contents ).replace ("\n " , newline_type )
1389- else :
1390- return "" .join (dst_contents )
1405+ if "\n " in normalized_contents :
1406+ return newline_type
1407+ return "" .join (dst_contents ).replace ("\n " , newline_type )
13911408
13921409
1393- def decode_bytes (src : bytes , mode : Mode ) -> tuple [FileContent , Encoding , NewLine ]:
1410+ def decode_bytes (
1411+ src : bytes , mode : Mode , * , encoding_overwrite : str | None = None
1412+ ) -> tuple [FileContent , Encoding , NewLine ]:
13941413 """Return a tuple of (decoded_contents, encoding, newline).
13951414
1396- `newline` is either CRLF or LF but `decoded_contents` is decoded with
1415+ `newline` is either CRLF, LF, or CR, but `decoded_contents` is decoded with
13971416 universal newlines (i.e. only contains LF).
1417+
1418+ Use the keyword only encoding_overwrite argument if the bytes are encoded
1419+ differently to their possible encoding magic comment.
13981420 """
13991421 srcbuf = io .BytesIO (src )
1422+
1423+ # Still use detect encoding even if overrite set because otherwise lines
1424+ # might be different
14001425 encoding , lines = tokenize .detect_encoding (srcbuf .readline )
1426+ if encoding_overwrite is not None :
1427+ encoding = encoding_overwrite
1428+
14011429 if not lines :
14021430 return "" , encoding , "\n "
14031431
1404- if Preview .normalize_cr_newlines in mode :
1405- if lines [0 ][- 2 :] == b"\r \n " :
1406- if b"\r " in lines [0 ][:- 2 ]:
1407- newline = "\r "
1408- else :
1409- newline = "\r \n "
1410- elif lines [0 ][- 1 :] == b"\n " :
1411- if b"\r " in lines [0 ][:- 1 ]:
1412- newline = "\r "
1413- else :
1414- newline = "\n "
1432+ if lines [0 ][- 2 :] == b"\r \n " :
1433+ if b"\r " in lines [0 ][:- 2 ]:
1434+ newline = "\r "
14151435 else :
1416- if b"\r " in lines [0 ]:
1417- newline = "\r "
1418- else :
1419- newline = "\n "
1436+ newline = "\r \n "
1437+ elif lines [0 ][- 1 :] == b"\n " :
1438+ if b"\r " in lines [0 ][:- 1 ]:
1439+ newline = "\r "
1440+ else :
1441+ newline = "\n "
14201442 else :
1421- newline = "\r \n " if lines [0 ][- 2 :] == b"\r \n " else "\n "
1443+ if b"\r " in lines [0 ]:
1444+ newline = "\r "
1445+ else :
1446+ newline = "\n "
14221447
14231448 srcbuf .seek (0 )
14241449 with io .TextIOWrapper (srcbuf , encoding ) as tiow :
0 commit comments