Skip to content

Commit 0e15a34

Browse files
pierrechevalier83ity
authored andcommitted
Add lint-v2 implementation for python with black, and script to be used in pre-commit hook (#8553)
* Add lint-v2 implementation for python with black, and script to be used in pre-commit hook
1 parent 5750037 commit 0e15a34

File tree

8 files changed

+253
-35
lines changed

8 files changed

+253
-35
lines changed

build-support/bin/BUILD

+6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ files(
1414
sources = globs('*.py'),
1515
)
1616

17+
python_binary(
18+
name = 'black',
19+
source = 'black.py',
20+
tags = {'type_checked'},
21+
)
22+
1723
python_binary(
1824
name = 'check_banned_imports',
1925
source = 'check_banned_imports.py',

build-support/bin/black.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md).
3+
# Licensed under the Apache License, Version 2.0 (see LICENSE).
4+
5+
import argparse
6+
import subprocess
7+
import sys
8+
9+
from common import git_merge_base
10+
11+
12+
def create_parser() -> argparse.ArgumentParser:
13+
parser = argparse.ArgumentParser(
14+
description="Formats all python files since master with black through pants.",
15+
)
16+
parser.add_argument("-f", "--fix", action="store_true",
17+
help="Instead of erroring on files with incorrect formatting, fix those files."
18+
)
19+
return parser
20+
21+
22+
def main() -> None:
23+
args= create_parser().parse_args()
24+
merge_base = git_merge_base()
25+
goal = "fmt-v2" if args.fix else "lint-v2"
26+
command = ["./pants", f"--changed-parent={merge_base}", goal]
27+
process = subprocess.run(command)
28+
sys.exit(process.returncode)
29+
30+
if __name__ == "__main__":
31+
main()

build-support/bin/common.py

+12
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
Callers of this file, however, are free to dogfood Pants as they'd like, and any script
2020
may be called via `./pants run` instead of direct invocation if desired."""
2121

22+
import subprocess
2223
import time
2324
from contextlib import contextmanager
2425
from pathlib import Path
@@ -53,6 +54,17 @@ def elapsed_time() -> Tuple[int, int]:
5354
return elapsed_seconds // 60, elapsed_seconds % 60
5455

5556

57+
def git_merge_base() -> str:
58+
get_tracking_branch = ["git",
59+
"rev-parse",
60+
"--symbolic-full-name",
61+
"--abbrev-ref",
62+
"HEAD@{upstream}"
63+
]
64+
process = subprocess.run(get_tracking_branch, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8")
65+
return str(process.stdout.rstrip()) if process.stdout else "master"
66+
67+
5668
@contextmanager
5769
def travis_section(slug: str, message: str) -> Iterator[None]:
5870
travis_fold_state_path = Path("/tmp/.travis_fold_current")

src/python/pants/backend/python/rules/python_fmt.py

+79-18
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44
import re
55
from dataclasses import dataclass
66
from pathlib import Path
7-
from typing import Any, Set
7+
from typing import Any, Set, Tuple
88

99
from pants.backend.python.rules.pex import CreatePex, Pex, PexInterpreterContraints, PexRequirements
1010
from pants.backend.python.subsystems.black import Black
1111
from pants.backend.python.subsystems.python_setup import PythonSetup
1212
from pants.backend.python.subsystems.subprocess_environment import SubprocessEncodingEnvironment
1313
from pants.engine.fs import Digest, DirectoriesToMerge, PathGlobs, Snapshot
14-
from pants.engine.isolated_process import ExecuteProcessRequest, ExecuteProcessResult
14+
from pants.engine.isolated_process import (
15+
ExecuteProcessRequest,
16+
ExecuteProcessResult,
17+
FallibleExecuteProcessResult,
18+
)
1519
from pants.engine.legacy.structs import (
1620
PantsPluginAdaptor,
1721
PythonAppAdaptor,
@@ -21,7 +25,8 @@
2125
)
2226
from pants.engine.rules import UnionRule, optionable_rule, rule
2327
from pants.engine.selectors import Get
24-
from pants.rules.core.fmt import FmtResult, FmtTarget
28+
from pants.rules.core.fmt import FmtResult, TargetWithSources
29+
from pants.rules.core.lint import LintResult
2530

2631

2732
# Note: this is a workaround until https://github.com/pantsbuild/pants/issues/8343 is addressed
@@ -32,13 +37,18 @@ class FormattablePythonTarget:
3237
target: Any
3338

3439

40+
@dataclass(frozen=True)
41+
class BlackInput:
42+
config_path: Path
43+
resolved_requirements_pex: Pex
44+
merged_input_files: Digest
45+
46+
3547
@rule
36-
def run_black(
48+
def get_black_input(
3749
wrapped_target: FormattablePythonTarget,
3850
black: Black,
39-
python_setup: PythonSetup,
40-
subprocess_encoding_environment: SubprocessEncodingEnvironment,
41-
) -> FmtResult:
51+
) -> BlackInput:
4252
config_path = black.get_options().config
4353
config_snapshot = yield Get(Snapshot, PathGlobs(include=(config_path,)))
4454

@@ -62,28 +72,58 @@ def run_black(
6272
Digest,
6373
DirectoriesToMerge(directories=tuple(all_input_digests)),
6474
)
75+
yield BlackInput(config_path, resolved_requirements_pex, merged_input_files)
6576

77+
78+
def _generate_black_pex_args(files: Set[str], config_path: str, *, check_only: bool) -> Tuple[str, ...]:
6679
# The exclude option from Black only works on recursive invocations,
6780
# so call black with the directories in which the files are present
6881
# and passing the full file names with the include option
6982
dirs: Set[str] = set()
70-
for filename in target.sources.snapshot.files:
83+
for filename in files:
7184
dirs.add(f"{Path(filename).parent}")
7285
pex_args= tuple(sorted(dirs))
86+
if check_only:
87+
pex_args += ("--check", )
7388
if config_path:
7489
pex_args += ("--config", config_path)
75-
if target.sources.snapshot.files:
76-
pex_args += ("--include", "|".join(re.escape(f) for f in target.sources.snapshot.files))
90+
if files:
91+
pex_args += ("--include", "|".join(re.escape(f) for f in files))
92+
return pex_args
93+
7794

78-
request = resolved_requirements_pex.create_execute_request(
95+
def _generate_black_request(
96+
wrapped_target: FormattablePythonTarget,
97+
black_input: BlackInput,
98+
python_setup: PythonSetup,
99+
subprocess_encoding_environment: SubprocessEncodingEnvironment,
100+
*,
101+
check_only: bool,
102+
):
103+
target = wrapped_target.target
104+
pex_args = _generate_black_pex_args(target.sources.snapshot.files, black_input.config_path, check_only = check_only)
105+
106+
request = black_input.resolved_requirements_pex.create_execute_request(
79107
python_setup=python_setup,
80108
subprocess_encoding_environment=subprocess_encoding_environment,
81109
pex_path="./black.pex",
82110
pex_args=pex_args,
83-
input_files=merged_input_files,
111+
input_files=black_input.merged_input_files,
84112
output_files=target.sources.snapshot.files,
85113
description=f'Run Black for {target.address.reference()}',
86114
)
115+
return request
116+
117+
118+
@rule
119+
def fmt_with_black(
120+
wrapped_target: FormattablePythonTarget,
121+
black_input: BlackInput,
122+
python_setup: PythonSetup,
123+
subprocess_encoding_environment: SubprocessEncodingEnvironment,
124+
) -> FmtResult:
125+
126+
request = _generate_black_request(wrapped_target, black_input, python_setup, subprocess_encoding_environment, check_only = False)
87127

88128
result = yield Get(ExecuteProcessResult, ExecuteProcessRequest, request)
89129

@@ -94,6 +134,25 @@ def run_black(
94134
)
95135

96136

137+
@rule
138+
def lint_with_black(
139+
wrapped_target: FormattablePythonTarget,
140+
black_input: BlackInput,
141+
python_setup: PythonSetup,
142+
subprocess_encoding_environment: SubprocessEncodingEnvironment,
143+
) -> LintResult:
144+
145+
request = _generate_black_request(wrapped_target, black_input, python_setup, subprocess_encoding_environment, check_only = True)
146+
147+
result = yield Get(FallibleExecuteProcessResult, ExecuteProcessRequest, request)
148+
149+
yield LintResult(
150+
exit_code=result.exit_code,
151+
stdout=result.stdout.decode(),
152+
stderr=result.stderr.decode(),
153+
)
154+
155+
97156
# TODO: remove this workaround once https://github.com/pantsbuild/pants/issues/8343 is addressed
98157
@rule
99158
def target_adaptor(target: PythonTargetAdaptor) -> FormattablePythonTarget:
@@ -131,12 +190,14 @@ def rules():
131190
binary_adaptor,
132191
tests_adaptor,
133192
plugin_adaptor,
134-
run_black,
135-
UnionRule(FmtTarget, PythonTargetAdaptor),
136-
UnionRule(FmtTarget, PythonAppAdaptor),
137-
UnionRule(FmtTarget, PythonBinaryAdaptor),
138-
UnionRule(FmtTarget, PythonTestsAdaptor),
139-
UnionRule(FmtTarget, PantsPluginAdaptor),
193+
get_black_input,
194+
fmt_with_black,
195+
lint_with_black,
196+
UnionRule(TargetWithSources, PythonTargetAdaptor),
197+
UnionRule(TargetWithSources, PythonAppAdaptor),
198+
UnionRule(TargetWithSources, PythonBinaryAdaptor),
199+
UnionRule(TargetWithSources, PythonTestsAdaptor),
200+
UnionRule(TargetWithSources, PantsPluginAdaptor),
140201
optionable_rule(Black),
141202
optionable_rule(PythonSetup),
142203
]

src/python/pants/rules/core/fmt.py

+7-17
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,7 @@
99
from pants.engine.fs import Digest, FilesContent
1010
from pants.engine.goal import Goal
1111
from pants.engine.legacy.graph import HydratedTargets
12-
from pants.engine.legacy.structs import (
13-
PythonAppAdaptor,
14-
PythonBinaryAdaptor,
15-
PythonTargetAdaptor,
16-
PythonTestsAdaptor,
17-
)
18-
from pants.engine.rules import console_rule, union
12+
from pants.engine.rules import UnionMembership, console_rule, union
1913
from pants.engine.selectors import Get
2014

2115

@@ -27,7 +21,7 @@ class FmtResult:
2721

2822

2923
@union
30-
class FmtTarget:
24+
class TargetWithSources:
3125
"""A union for registration of a formattable target type."""
3226

3327

@@ -40,17 +34,13 @@ class Fmt(Goal):
4034

4135

4236
@console_rule
43-
def fmt(console: Console, targets: HydratedTargets) -> Fmt:
37+
def fmt(console: Console, targets: HydratedTargets, union_membership: UnionMembership) -> Fmt:
4438
results = yield [
45-
Get(FmtResult, FmtTarget, target.adaptor)
39+
Get(FmtResult, TargetWithSources, target.adaptor)
4640
for target in targets
47-
# @union assumes that all targets passed implement the union, so we manually
48-
# filter the targets we know do; this should probably no-op or log or something
49-
# configurable for non-matching targets.
50-
# We also would want to remove the workaround that filters adaptors which have a
51-
# `sources` attribute.
52-
# See https://github.com/pantsbuild/pants/issues/4535
53-
if isinstance(target.adaptor, (PythonAppAdaptor, PythonTargetAdaptor, PythonTestsAdaptor, PythonBinaryAdaptor)) and hasattr(target.adaptor, "sources")
41+
# TODO: make TargetAdaptor return a 'sources' field with an empty snapshot instead of
42+
# raising to remove the hasattr() checks here!
43+
if union_membership.is_member(TargetWithSources, target.adaptor) and hasattr(target.adaptor, "sources")
5444
]
5545

5646
for result in results:

src/python/pants/rules/core/lint.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md).
2+
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3+
4+
from dataclasses import dataclass
5+
6+
from pants.engine.console import Console
7+
from pants.engine.goal import Goal
8+
from pants.engine.legacy.graph import HydratedTargets
9+
from pants.engine.rules import UnionMembership, console_rule
10+
from pants.engine.selectors import Get
11+
from pants.rules.core.fmt import TargetWithSources
12+
13+
14+
@dataclass(frozen=True)
15+
class LintResult:
16+
exit_code: int
17+
stdout: str
18+
stderr: str
19+
20+
21+
class Lint(Goal):
22+
"""Lint source code."""
23+
24+
# TODO: make this "lint"
25+
# Blocked on https://github.com/pantsbuild/pants/issues/8351
26+
name = 'lint-v2'
27+
28+
29+
@console_rule
30+
def lint(console: Console, targets: HydratedTargets, union_membership: UnionMembership) -> Lint:
31+
results = yield [
32+
Get(LintResult, TargetWithSources, target.adaptor)
33+
for target in targets
34+
# TODO: make TargetAdaptor return a 'sources' field with an empty snapshot instead of
35+
# raising to remove the hasattr() checks here!
36+
if union_membership.is_member(TargetWithSources, target.adaptor) and hasattr(target.adaptor, "sources")
37+
]
38+
39+
exit_code = 0
40+
for result in results:
41+
if result.stdout:
42+
console.print_stdout(result.stdout)
43+
if result.stderr:
44+
console.print_stderr(result.stderr)
45+
if result.exit_code != 0:
46+
exit_code = result.exit_code
47+
48+
yield Lint(exit_code)
49+
50+
51+
def rules():
52+
return [
53+
lint,
54+
]

src/python/pants/rules/core/register.py

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
binary,
66
filedeps,
77
fmt,
8+
lint,
89
list_roots,
910
list_targets,
1011
strip_source_root,
@@ -16,6 +17,7 @@ def rules():
1617
return [
1718
*binary.rules(),
1819
*fmt.rules(),
20+
*lint.rules(),
1921
*list_roots.rules(),
2022
*list_targets.rules(),
2123
*filedeps.rules(),

0 commit comments

Comments
 (0)