Skip to content

Commit d5b3796

Browse files
The Pyink Maintainerscopybara-github
authored andcommitted
Rebase Pyink to Black (https://github.com/psf/black) v26.3.1.
Commented out a code block in lines.py to allow 2 blank lines after imports. Temporarily disabled: test_simple_format[import_line_collapse] PiperOrigin-RevId: 885483341
1 parent 14c3f17 commit d5b3796

51 files changed

Lines changed: 1352 additions & 981 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

patches/pyink.patch

Lines changed: 205 additions & 212 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 90 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Yes, we use the _Black_ style to format _Pyink_ code.
33
pyink = false
44
line-length = 88
5-
target-version = ['py310']
5+
target-version = ["py310"]
66
include = '\.pyi?$'
77
extend-exclude = 'tests/data'
88
unstable = true
@@ -20,40 +20,73 @@ requires-python = ">=3.10"
2020
readme = "README.md"
2121
authors = [{name = "The Pyink Maintainers", email = "pyink-maintainers@google.com"}]
2222
classifiers = [
23-
"Development Status :: 5 - Production/Stable",
24-
"Environment :: Console",
25-
"Intended Audience :: Developers",
26-
"Operating System :: OS Independent",
27-
"Programming Language :: Python",
28-
"Programming Language :: Python :: 3 :: Only",
29-
"Programming Language :: Python :: 3.10",
30-
"Programming Language :: Python :: 3.11",
31-
"Programming Language :: Python :: 3.12",
32-
"Programming Language :: Python :: 3.13",
33-
"Programming Language :: Python :: 3.14",
34-
"Topic :: Software Development :: Libraries :: Python Modules",
35-
"Topic :: Software Development :: Quality Assurance",
23+
"Development Status :: 5 - Production/Stable",
24+
"Environment :: Console",
25+
"Intended Audience :: Developers",
26+
"Operating System :: OS Independent",
27+
"Programming Language :: Python",
28+
"Programming Language :: Python :: 3 :: Only",
29+
"Programming Language :: Python :: 3.10",
30+
"Programming Language :: Python :: 3.11",
31+
"Programming Language :: Python :: 3.12",
32+
"Programming Language :: Python :: 3.13",
33+
"Programming Language :: Python :: 3.14",
34+
"Topic :: Software Development :: Libraries :: Python Modules",
35+
"Topic :: Software Development :: Quality Assurance",
3636
]
3737
dependencies = [
38-
"click>=8.0.0",
39-
"mypy_extensions>=0.4.3",
40-
"packaging>=22.0",
41-
"pathspec>=0.9.0,<1.0.0",
42-
"platformdirs>=2",
43-
"pytokens>=0.3.0",
44-
"tomli>=1.1.0; python_version < '3.11'",
45-
"typing_extensions>=4.0.1; python_version < '3.11'",
46-
"black==25.12.0",
38+
"click>=8.0.0",
39+
"mypy-extensions>=0.4.3",
40+
"packaging>=22.0",
41+
"pathspec>=1.0.0",
42+
"platformdirs>=2",
43+
"pytokens~=0.4.0",
44+
"tomli>=1.1.0; python_version<'3.11'",
45+
"typing-extensions>=4.0.1; python_version<'3.11'",
46+
"black==26.3.1",
4747
]
4848
dynamic = ["version"]
4949

5050
[project.optional-dependencies]
5151
colorama = ["colorama>=0.4.3"]
52-
uvloop = ["uvloop>=0.15.2"]
53-
jupyter = [
54-
"ipython>=7.8.0",
55-
"tokenize-rt>=3.2.0",
52+
uvloop = [
53+
"uvloop>=0.15.2; sys_platform != 'win32'",
54+
"winloop>=0.5.0; sys_platform == 'win32'"
5655
]
56+
d = ["aiohttp>=3.10"]
57+
jupyter = ["ipython>=7.8.0", "tokenize-rt>=3.2.0"]
58+
59+
[dependency-groups]
60+
build = ["hatch==1.15.1", "hatch-fancy-pypi-readme", "hatch-vcs>=0.3.0", "virtualenv<21.0.0"]
61+
wheels = ["cibuildwheel==3.3.1", "pypyp"]
62+
binary = ["pyinstaller", "wheel>=0.45.1"]
63+
64+
dev = [{ include-group = "cov-tests" }, { include-group = "tox" }, "pre-commit"]
65+
cov-tests = [
66+
{ include-group = "coverage" },
67+
{ include-group = "tests" },
68+
"pytest-cov>=4.1.0",
69+
]
70+
docs = [
71+
"docutils==0.21.2",
72+
"furo==2025.12.19",
73+
"myst-parser==4.0.1",
74+
"sphinx-copybutton==0.5.2",
75+
"sphinx==8.2.3",
76+
"sphinxcontrib-programoutput==0.19",
77+
]
78+
79+
tox = ["tox>=4.22"]
80+
tests = ["pytest>=7", "pytest-xdist>=3.0.2"]
81+
coverage = ["coverage>=5.3"]
82+
83+
fuzz = [{ include-group = "coverage" }, "hypothesis", "hypothesmith"]
84+
diff-shades = [
85+
"diff-shades @ https://github.com/ichard26/diff-shades/archive/stable.zip",
86+
]
87+
diff-shades-comment = ["click>=8.1.7", "packaging>=22.0", "urllib3"]
88+
89+
width-table = ["wcwidth==0.2.14"]
5790

5891
[project.scripts]
5992
pyink = "pyink:patched_main"
@@ -71,9 +104,9 @@ source = "vcs"
71104

72105
[tool.hatch.build.hooks.vcs]
73106
version-file = "src/_pyink_version.py"
74-
template = '''
107+
template = """
75108
version = "{version}"
76-
'''
109+
"""
77110

78111
[tool.hatch.build.targets.wheel]
79112
only-include = ["src"]
@@ -85,10 +118,34 @@ macos-max-compat = true
85118
# Option below requires `tests/optional.py`
86119
addopts = "--strict-config --strict-markers"
87120
optional-tests = [
88-
"no_jupyter: run when `jupyter` extra NOT installed",
89-
]
90-
markers = [
91-
"incompatible_with_mypyc: run when testing mypyc compiled black"
121+
"no_jupyter: run when `jupyter` extra NOT installed",
92122
]
123+
markers = ["incompatible_with_mypyc: run when testing mypyc compiled black"]
93124
xfail_strict = true
94125
filterwarnings = ["error"]
126+
127+
128+
[tool.mypy]
129+
# Specify the target platform details in config, so your developers are
130+
# free to run mypy on Windows, Linux, or macOS and get consistent
131+
# results.
132+
python_version = "3.10"
133+
mypy_path = "src"
134+
strict = true
135+
strict_bytes = true
136+
local_partial_types = true
137+
# Unreachable blocks have been an issue when compiling mypyc, let's try to avoid 'em in the first place.
138+
warn_unreachable = true
139+
implicit_reexport = true
140+
show_error_codes = true
141+
show_column_numbers = true
142+
143+
[[tool.mypy.overrides]]
144+
module = ["pathspec.*", "IPython.*", "colorama.*", "tokenize_rt.*", "uvloop.*"]
145+
ignore_missing_imports = true
146+
147+
# CI only checks src/, but in case users are running LSP or similar we explicitly ignore
148+
# errors in test data files.
149+
[[tool.mypy.overrides]]
150+
module = ["tests.data.*"]
151+
ignore_errors = true

src/pyink/__init__.py

Lines changed: 82 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
import click
2626
from click.core import ParameterSource
2727
from 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

3131
from _pyink_version import version as __version__
3232
from 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+
212233
def 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(
13141345
def _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

Comments
 (0)