-
-
Notifications
You must be signed in to change notification settings - Fork 652
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add black support as a v2 rule #8350
Changes from all commits
4338d91
6cae51a
54d3f70
95c709c
bdd2b9e
de8b953
ec738a5
4de25e7
7cbd4a0
36dfc19
4a3e893
f8ef9de
ddca228
c6887fa
018b6cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
[tool.black] | ||
line-length = 100 | ||
exclude = ''' | ||
/( | ||
# These would already be ignored by pants, but having them here allows for manually running Black if one so whishes. | ||
| \.git | ||
| \.mypy_cache | ||
| dist | ||
| \.pants\.d | ||
| virtualenvs | ||
# This file intentionally contains invalid syntax | ||
# It trips black up. | ||
| compilation_failure | ||
)/ | ||
''' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
import re | ||
from dataclasses import dataclass | ||
from pathlib import Path | ||
from typing import Any, Set | ||
|
||
from pants.backend.python.rules.pex import CreatePex, Pex | ||
from pants.backend.python.subsystems.black import Black | ||
from pants.backend.python.subsystems.python_setup import PythonSetup | ||
from pants.backend.python.subsystems.subprocess_environment import SubprocessEncodingEnvironment | ||
from pants.engine.fs import Digest, DirectoriesToMerge, PathGlobs, Snapshot | ||
from pants.engine.isolated_process import ExecuteProcessRequest, ExecuteProcessResult | ||
from pants.engine.legacy.structs import ( | ||
PantsPluginAdaptor, | ||
PythonAppAdaptor, | ||
PythonBinaryAdaptor, | ||
PythonTargetAdaptor, | ||
PythonTestsAdaptor, | ||
) | ||
from pants.engine.rules import UnionRule, optionable_rule, rule | ||
from pants.engine.selectors import Get | ||
from pants.rules.core.fmt import FmtResult, FmtTarget | ||
|
||
|
||
# Note: this is a workaround until https://github.com/pantsbuild/pants/issues/8343 is addressed | ||
# We have to write this type which basically represents a union of all various kinds of targets | ||
# containing python files so we can have one single type used as an input in the run_black rule. | ||
@dataclass(frozen=True) | ||
class FormattablePythonTarget: | ||
target: Any | ||
|
||
Eric-Arellano marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@rule | ||
def run_black( | ||
wrapped_target: FormattablePythonTarget, | ||
black: Black, | ||
python_setup: PythonSetup, | ||
subprocess_encoding_environment: SubprocessEncodingEnvironment, | ||
) -> FmtResult: | ||
config_path = black.get_options().config | ||
config_snapshot = yield Get(Snapshot, PathGlobs(include=(config_path,))) | ||
|
||
resolved_requirements_pex = yield Get( | ||
Pex, CreatePex( | ||
output_filename="black.pex", | ||
requirements=tuple(black.get_requirement_specs()), | ||
interpreter_constraints=tuple(black.default_interpreter_constraints), | ||
entry_point=black.get_entry_point(), | ||
) | ||
) | ||
target = wrapped_target.target | ||
sources_digest = target.sources.snapshot.directory_digest | ||
|
||
all_input_digests = [ | ||
sources_digest, | ||
resolved_requirements_pex.directory_digest, | ||
config_snapshot.directory_digest, | ||
] | ||
merged_input_files = yield Get( | ||
Digest, | ||
DirectoriesToMerge(directories=tuple(all_input_digests)), | ||
) | ||
|
||
# The exclude option from Black only works on recursive invocations, | ||
# so call black with the directories in which the files are present | ||
# and passing the full file names with the include option | ||
dirs: Set[str] = set() | ||
for filename in target.sources.snapshot.files: | ||
dirs.add(f"{Path(filename).parent}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for these changes :) |
||
pex_args= tuple(sorted(dirs)) | ||
if config_path: | ||
pex_args += ("--config", config_path) | ||
if target.sources.snapshot.files: | ||
pex_args += ("--include", "|".join(re.escape(f) for f in target.sources.snapshot.files)) | ||
|
||
request = resolved_requirements_pex.create_execute_request( | ||
python_setup=python_setup, | ||
subprocess_encoding_environment=subprocess_encoding_environment, | ||
pex_path="./black.pex", | ||
pex_args=pex_args, | ||
input_files=merged_input_files, | ||
output_files=target.sources.snapshot.files, | ||
description=f'Run Black for {target.address.reference()}', | ||
) | ||
|
||
result = yield Get(ExecuteProcessResult, ExecuteProcessRequest, request) | ||
|
||
yield FmtResult( | ||
digest=result.output_directory_digest, | ||
stdout=result.stdout.decode(), | ||
stderr=result.stderr.decode(), | ||
) | ||
|
||
# TODO: remove this workaround once https://github.com/pantsbuild/pants/issues/8343 is addressed | ||
@rule | ||
stuhood marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def target_adaptor(target: PythonTargetAdaptor) -> FormattablePythonTarget: | ||
yield FormattablePythonTarget(target) | ||
|
||
|
||
# TODO: remove this workaround once https://github.com/pantsbuild/pants/issues/8343 is addressed | ||
@rule | ||
def app_adaptor(target: PythonAppAdaptor) -> FormattablePythonTarget: | ||
yield FormattablePythonTarget(target) | ||
|
||
|
||
# TODO: remove this workaround once https://github.com/pantsbuild/pants/issues/8343 is addressed | ||
@rule | ||
def binary_adaptor(target: PythonBinaryAdaptor) -> FormattablePythonTarget: | ||
yield FormattablePythonTarget(target) | ||
|
||
|
||
# TODO: remove this workaround once https://github.com/pantsbuild/pants/issues/8343 is addressed | ||
@rule | ||
def tests_adaptor(target: PythonTestsAdaptor) -> FormattablePythonTarget: | ||
yield FormattablePythonTarget(target) | ||
|
||
|
||
# TODO: remove this workaround once https://github.com/pantsbuild/pants/issues/8343 is addressed | ||
@rule | ||
def plugin_adaptor(target: PantsPluginAdaptor) -> FormattablePythonTarget: | ||
yield FormattablePythonTarget(target) | ||
|
||
|
||
def rules(): | ||
return [ | ||
target_adaptor, | ||
app_adaptor, | ||
binary_adaptor, | ||
tests_adaptor, | ||
plugin_adaptor, | ||
run_black, | ||
UnionRule(FmtTarget, PythonTargetAdaptor), | ||
UnionRule(FmtTarget, PythonAppAdaptor), | ||
UnionRule(FmtTarget, PythonBinaryAdaptor), | ||
UnionRule(FmtTarget, PythonTestsAdaptor), | ||
UnionRule(FmtTarget, PantsPluginAdaptor), | ||
optionable_rule(Black), | ||
optionable_rule(PythonSetup), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
from pants.backend.python.subsystems.python_tool_base import PythonToolBase | ||
from pants.option.custom_types import file_option | ||
|
||
|
||
class Black(PythonToolBase): | ||
options_scope = 'black' | ||
default_requirements = ['black==19.3b0', 'setuptools'] | ||
default_entry_point = 'black:patched_main' | ||
default_interpreter_constraints = ["CPython>=3.6"] | ||
|
||
@classmethod | ||
def register_options(cls, register): | ||
super().register_options(register) | ||
register('--config', advanced=True, type=file_option, fingerprint=True, | ||
help="Path to formatting tool's config file") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
from dataclasses import dataclass | ||
from pathlib import Path | ||
|
||
from pants.base.build_environment import get_buildroot | ||
from pants.engine.console import Console | ||
from pants.engine.fs import Digest, FilesContent | ||
from pants.engine.goal import Goal | ||
from pants.engine.legacy.graph import HydratedTargets | ||
from pants.engine.legacy.structs import ( | ||
PythonAppAdaptor, | ||
PythonBinaryAdaptor, | ||
PythonTargetAdaptor, | ||
PythonTestsAdaptor, | ||
) | ||
from pants.engine.rules import console_rule, union | ||
from pants.engine.selectors import Get | ||
from pants.util.objects import datatype | ||
|
||
|
||
@dataclass(frozen=True) | ||
class FmtResult: | ||
digest: Digest | ||
stdout: str | ||
stderr: str | ||
|
||
|
||
@union | ||
class FmtTarget: | ||
"""A union for registration of a formattable target type.""" | ||
|
||
|
||
class Fmt(Goal): | ||
"""Autoformat source code.""" | ||
|
||
# TODO: make this "fmt" | ||
# Blocked on https://github.com/pantsbuild/pants/issues/8351 | ||
name = 'fmt-v2' | ||
|
||
|
||
@console_rule | ||
def fmt(console: Console, targets: HydratedTargets) -> Fmt: | ||
results = yield [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a TODO linking to #4535 saying something like " |
||
Get(FmtResult, FmtTarget, target.adaptor) | ||
for target in targets | ||
# @union assumes that all targets passed implement the union, so we manually | ||
# filter the targets we know do; this should probably no-op or log or something | ||
# configurable for non-matching targets. | ||
# We also would want to remove the workaround that filters adaptors which have a | ||
# `sources` attribute. | ||
# See https://github.com/pantsbuild/pants/issues/4535 | ||
if isinstance(target.adaptor, (PythonAppAdaptor, PythonTargetAdaptor, PythonTestsAdaptor, PythonBinaryAdaptor)) and hasattr(target.adaptor, "sources") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a TODO linking to #4535 complaining about the |
||
] | ||
|
||
for result in results: | ||
files_content = yield Get(FilesContent, Digest, result.digest) | ||
# TODO: This is hacky and inefficient, and should be replaced by using the Workspace type | ||
# once that is available on master. | ||
# Blocked on: https://github.com/pantsbuild/pants/pull/8329 | ||
for file_content in files_content: | ||
with Path(get_buildroot(), file_content.path).open('wb') as f: | ||
f.write(file_content.content) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a TODO that we should use #8329 (and noting that the reason we're ok with this horribly inefficient implementation is because it will disappear soon :D) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. from pathlib import Path
Path(get_buildroot(), file_content.path).write_text(file_content.content.decode()) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or from pathlib import Path
with Path(get_buildroot(), file_content.path).open('wb') as f:
f.write(file_content.content) |
||
|
||
if result.stdout: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels likely to be noisy... but I suppose that if a tool knows that it won't produce anything interesting, it could skip including it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that can be made less verbose by batching the files later. I would probably not discard the output as I would want to see what happened as a user. |
||
console.print_stdout(result.stdout) | ||
if result.stderr: | ||
console.print_stderr(result.stderr) | ||
|
||
# Since we ran an ExecuteRequest, any failure would already have interrupted our flow | ||
exit_code = 0 | ||
yield Fmt(exit_code) | ||
|
||
|
||
def rules(): | ||
return [ | ||
fmt, | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From a pants perspective, pants already ignores these files when it expands globs; are they here in order to preserve the ability to just run
black
not via pants? If so, may be worth a comment to that effect