From 13fe2be3b1149c594a78c50106cea85a4e34482e Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Wed, 17 Aug 2022 13:54:22 -0500 Subject: [PATCH 01/15] Move `_find_python_interpreter_constraints_from_lockfile` # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- .../python/subsystems/python_tool_base.py | 37 ++++++++++++++++ .../pants/core/goals/update_build_files.py | 43 ++----------------- .../core/goals/update_build_files_test.py | 3 +- 3 files changed, 41 insertions(+), 42 deletions(-) diff --git a/src/python/pants/backend/python/subsystems/python_tool_base.py b/src/python/pants/backend/python/subsystems/python_tool_base.py index 5210c4bae77..18b123cf222 100644 --- a/src/python/pants/backend/python/subsystems/python_tool_base.py +++ b/src/python/pants/backend/python/subsystems/python_tool_base.py @@ -11,6 +11,8 @@ from pants.backend.python.util_rules.pex import PexRequest from pants.backend.python.util_rules.pex_requirements import ( EntireLockfile, + LoadedLockfile, + LoadedLockfileRequest, PexRequirements, ToolCustomLockfile, ToolDefaultLockfile, @@ -18,6 +20,8 @@ from pants.core.goals.generate_lockfiles import DEFAULT_TOOL_LOCKFILE, NO_TOOL_LOCKFILE from pants.core.util_rules.lockfile_metadata import calculate_invalidation_digest from pants.engine.fs import Digest, FileContent +from pants.engine.internals.selectors import Get +from pants.engine.rules import rule_helper from pants.option.errors import OptionsError from pants.option.option_types import BoolOption, StrListOption, StrOption from pants.option.subsystem import Subsystem @@ -294,6 +298,39 @@ def to_pex_request( sources=sources, ) + @staticmethod + @rule_helper + async def _find_python_interpreter_constraints_from_lockfile( + subsystem: PythonToolBase, + ) -> InterpreterConstraints: + """If a lockfile is used, will try to find the interpreter constraints used to generate the + lock. + + This allows us to work around https://github.com/pantsbuild/pants/issues/14912. + """ + # If the tool's interpreter constraints are explicitly set, or it is not using a lockfile at + # all, then we should use the tool's interpreter constraints option. + if ( + not subsystem.options.is_default("interpreter_constraints") + or not subsystem.uses_lockfile + ): + return subsystem.interpreter_constraints + + # If using Pants's default lockfile, we can simply use the tool's default interpreter + # constraints, which we trust were used to generate Pants's default tool lockfile. + if not subsystem.uses_custom_lockfile: + return InterpreterConstraints(subsystem.default_interpreter_constraints) + + # Else, try to load the metadata block from the lockfile. + requirements = subsystem.pex_requirements() + assert isinstance(requirements, EntireLockfile) + lockfile = await Get(LoadedLockfile, LoadedLockfileRequest(requirements.lockfile)) + return ( + lockfile.metadata.valid_for_interpreter_constraints + if lockfile.metadata + else subsystem.interpreter_constraints + ) + class ExportToolOption(BoolOption): """An `--export` option to toggle whether the `export` goal should include the tool.""" diff --git a/src/python/pants/core/goals/update_build_files.py b/src/python/pants/core/goals/update_build_files.py index 3e6892fd292..057e3b25669 100644 --- a/src/python/pants/core/goals/update_build_files.py +++ b/src/python/pants/core/goals/update_build_files.py @@ -17,15 +17,8 @@ from pants.backend.python.lint.black.subsystem import Black from pants.backend.python.lint.yapf.subsystem import Yapf -from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.util_rules import pex -from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints from pants.backend.python.util_rules.pex import PexRequest, VenvPex, VenvPexProcess -from pants.backend.python.util_rules.pex_requirements import ( - EntireLockfile, - LoadedLockfile, - LoadedLockfileRequest, -) from pants.base.specs import Specs from pants.core.util_rules.config_files import ConfigFiles, ConfigFilesRequest from pants.engine.console import Console @@ -45,7 +38,7 @@ from pants.engine.internals.build_files import BuildFileOptions from pants.engine.internals.parser import ParseError from pants.engine.process import ProcessResult -from pants.engine.rules import Get, MultiGet, collect_rules, goal_rule, rule, rule_helper +from pants.engine.rules import Get, MultiGet, collect_rules, goal_rule, rule from pants.engine.target import RegisteredTargetTypes, TargetGenerator from pants.engine.unions import UnionMembership, UnionRule, union from pants.option.option_types import BoolOption, EnumOption @@ -300,36 +293,6 @@ async def update_build_files( return UpdateBuildFilesGoal(exit_code=1 if update_build_files_subsystem.check else 0) -@rule_helper -async def _find_python_interpreter_constraints_from_lockfile( - subsystem: PythonToolBase, -) -> InterpreterConstraints: - """If a lockfile is used, will try to find the interpreter constraints used to generate the - lock. - - This allows us to work around https://github.com/pantsbuild/pants/issues/14912. - """ - # If the tool's interpreter constraints are explicitly set, or it is not using a lockfile at - # all, then we should use the tool's interpreter constraints option. - if not subsystem.options.is_default("interpreter_constraints") or not subsystem.uses_lockfile: - return subsystem.interpreter_constraints - - # If using Pants's default lockfile, we can simply use the tool's default interpreter - # constraints, which we trust were used to generate Pants's default tool lockfile. - if not subsystem.uses_custom_lockfile: - return InterpreterConstraints(subsystem.default_interpreter_constraints) - - # Else, try to load the metadata block from the lockfile. - requirements = subsystem.pex_requirements() - assert isinstance(requirements, EntireLockfile) - lockfile = await Get(LoadedLockfile, LoadedLockfileRequest(requirements.lockfile)) - return ( - lockfile.metadata.valid_for_interpreter_constraints - if lockfile.metadata - else subsystem.interpreter_constraints - ) - - # ------------------------------------------------------------------------------------------ # Yapf formatter fixer # ------------------------------------------------------------------------------------------ @@ -343,7 +306,7 @@ class FormatWithYapfRequest(RewrittenBuildFileRequest): async def format_build_file_with_yapf( request: FormatWithYapfRequest, yapf: Yapf ) -> RewrittenBuildFile: - yapf_ics = await _find_python_interpreter_constraints_from_lockfile(yapf) + yapf_ics = await Yapf._find_python_interpreter_constraints_from_lockfile(yapf) yapf_pex_get = Get(VenvPex, PexRequest, yapf.to_pex_request(interpreter_constraints=yapf_ics)) build_file_digest_get = Get(Digest, CreateDigest([request.to_file_content()])) config_files_get = Get( @@ -397,7 +360,7 @@ class FormatWithBlackRequest(RewrittenBuildFileRequest): async def format_build_file_with_black( request: FormatWithBlackRequest, black: Black ) -> RewrittenBuildFile: - black_ics = await _find_python_interpreter_constraints_from_lockfile(black) + black_ics = await Black._find_python_interpreter_constraints_from_lockfile(black) black_pex_get = Get( VenvPex, PexRequest, black.to_pex_request(interpreter_constraints=black_ics) ) diff --git a/src/python/pants/core/goals/update_build_files_test.py b/src/python/pants/core/goals/update_build_files_test.py index 9a34b03035b..00ffbfaeb0a 100644 --- a/src/python/pants/core/goals/update_build_files_test.py +++ b/src/python/pants/core/goals/update_build_files_test.py @@ -30,7 +30,6 @@ RewrittenBuildFileRequest, UpdateBuildFilesGoal, UpdateBuildFilesSubsystem, - _find_python_interpreter_constraints_from_lockfile, determine_renamed_field_types, format_build_file_with_black, format_build_file_with_yapf, @@ -179,7 +178,7 @@ def assert_ics( ), ) result = run_rule_with_mocks( - _find_python_interpreter_constraints_from_lockfile, + Black._find_python_interpreter_constraints_from_lockfile, rule_args=[black], mock_gets=[ MockGet( From d16a28d8ef347431202adf0fedc4d37cfc00fa61 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Wed, 17 Aug 2022 13:55:42 -0500 Subject: [PATCH 02/15] Add helpers to run black/yapf # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- .../pants/backend/python/lint/black/rules.py | 85 +++++++++++-------- .../pants/backend/python/lint/yapf/rules.py | 30 +++++-- 2 files changed, 70 insertions(+), 45 deletions(-) diff --git a/src/python/pants/backend/python/lint/black/rules.py b/src/python/pants/backend/python/lint/black/rules.py index 7d5b2fe73f7..2d2718f2aac 100644 --- a/src/python/pants/backend/python/lint/black/rules.py +++ b/src/python/pants/backend/python/lint/black/rules.py @@ -1,6 +1,8 @@ # Copyright 2019 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). +from __future__ import annotations + from dataclasses import dataclass from pants.backend.python.lint.black.skip_field import SkipBlackField @@ -10,12 +12,12 @@ from pants.backend.python.util_rules import pex from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints from pants.backend.python.util_rules.pex import PexRequest, VenvPex, VenvPexProcess -from pants.core.goals.fmt import FmtResult, FmtTargetsRequest +from pants.core.goals.fmt import FmtResult, FmtTargetsRequest, _FmtBuildFilesRequest from pants.core.util_rules.config_files import ConfigFiles, ConfigFilesRequest from pants.engine.fs import Digest, MergeDigests from pants.engine.internals.native_engine import Snapshot from pants.engine.process import ProcessResult -from pants.engine.rules import Get, MultiGet, collect_rules, rule +from pants.engine.rules import Get, MultiGet, collect_rules, rule, rule_helper from pants.engine.target import FieldSet, Target from pants.engine.unions import UnionRule from pants.util.logging import LogLevel @@ -39,6 +41,49 @@ class BlackRequest(FmtTargetsRequest): name = Black.options_scope +@rule_helper +async def _run_black( + request: FmtTargetsRequest | _FmtBuildFilesRequest, + black: Black, + interpreter_constraints: InterpreterConstraints, +) -> FmtResult: + black_pex_get = Get( + VenvPex, + PexRequest, + black.to_pex_request(interpreter_constraints=interpreter_constraints), + ) + config_files_get = Get( + ConfigFiles, ConfigFilesRequest, black.config_request(request.snapshot.dirs) + ) + + black_pex, config_files = await MultiGet(black_pex_get, config_files_get) + + input_digest = await Get( + Digest, MergeDigests((request.snapshot.digest, config_files.snapshot.digest)) + ) + + result = await Get( + ProcessResult, + VenvPexProcess( + black_pex, + argv=( + *(("--config", black.config) if black.config else ()), + "-W", + "{pants_concurrency}", + *black.args, + *request.snapshot.files, + ), + input_digest=input_digest, + output_files=request.snapshot.files, + concurrency_available=len(request.snapshot.files), + description=f"Run Black on {pluralize(len(request.snapshot.files), 'file')}.", + level=LogLevel.DEBUG, + ), + ) + output_snapshot = await Get(Snapshot, Digest, result.output_digest) + return FmtResult.create(request, result, output_snapshot, strip_chroot_path=True) + + @rule(desc="Format with Black", level=LogLevel.DEBUG) async def black_fmt(request: BlackRequest, black: Black, python_setup: PythonSetup) -> FmtResult: if black.skip: @@ -73,41 +118,7 @@ async def black_fmt(request: BlackRequest, black: Black, python_setup: PythonSet ): tool_interpreter_constraints = all_interpreter_constraints - black_pex_get = Get( - VenvPex, - PexRequest, - black.to_pex_request(interpreter_constraints=tool_interpreter_constraints), - ) - config_files_get = Get( - ConfigFiles, ConfigFilesRequest, black.config_request(request.snapshot.dirs) - ) - - black_pex, config_files = await MultiGet(black_pex_get, config_files_get) - - input_digest = await Get( - Digest, MergeDigests((request.snapshot.digest, config_files.snapshot.digest)) - ) - - result = await Get( - ProcessResult, - VenvPexProcess( - black_pex, - argv=( - *(("--config", black.config) if black.config else ()), - "-W", - "{pants_concurrency}", - *black.args, - *request.snapshot.files, - ), - input_digest=input_digest, - output_files=request.snapshot.files, - concurrency_available=len(request.field_sets), - description=f"Run Black on {pluralize(len(request.field_sets), 'file')}.", - level=LogLevel.DEBUG, - ), - ) - output_snapshot = await Get(Snapshot, Digest, result.output_digest) - return FmtResult.create(request, result, output_snapshot, strip_chroot_path=True) + return await _run_black(request, black, tool_interpreter_constraints) def rules(): diff --git a/src/python/pants/backend/python/lint/yapf/rules.py b/src/python/pants/backend/python/lint/yapf/rules.py index 483237a16ae..6251539b10a 100644 --- a/src/python/pants/backend/python/lint/yapf/rules.py +++ b/src/python/pants/backend/python/lint/yapf/rules.py @@ -1,19 +1,22 @@ # Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). +from __future__ import annotations + from dataclasses import dataclass from pants.backend.python.lint.yapf.skip_field import SkipYapfField from pants.backend.python.lint.yapf.subsystem import Yapf from pants.backend.python.target_types import PythonSourceField from pants.backend.python.util_rules import pex +from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints from pants.backend.python.util_rules.pex import PexRequest, VenvPex, VenvPexProcess -from pants.core.goals.fmt import FmtResult, FmtTargetsRequest +from pants.core.goals.fmt import FmtResult, FmtTargetsRequest, _FmtBuildFilesRequest from pants.core.util_rules.config_files import ConfigFiles, ConfigFilesRequest from pants.engine.fs import Digest, MergeDigests from pants.engine.internals.native_engine import Snapshot from pants.engine.process import ProcessResult -from pants.engine.rules import Get, MultiGet, collect_rules, rule +from pants.engine.rules import Get, MultiGet, collect_rules, rule, rule_helper from pants.engine.target import FieldSet, Target from pants.engine.unions import UnionRule from pants.util.logging import LogLevel @@ -36,11 +39,15 @@ class YapfRequest(FmtTargetsRequest): name = Yapf.options_scope -@rule(desc="Format with yapf", level=LogLevel.DEBUG) -async def yapf_fmt(request: YapfRequest, yapf: Yapf) -> FmtResult: - if yapf.skip: - return FmtResult.skip(formatter_name=request.name) - yapf_pex_get = Get(VenvPex, PexRequest, yapf.to_pex_request()) +@rule_helper +async def _run_yapf( + request: FmtTargetsRequest | _FmtBuildFilesRequest, + yapf: Yapf, + interpreter_constraints: InterpreterConstraints | None = None, +) -> FmtResult: + yapf_pex_get = Get( + VenvPex, PexRequest, yapf.to_pex_request(interpreter_constraints=interpreter_constraints) + ) config_files_get = Get( ConfigFiles, ConfigFilesRequest, yapf.config_request(request.snapshot.dirs) ) @@ -62,7 +69,7 @@ async def yapf_fmt(request: YapfRequest, yapf: Yapf) -> FmtResult: ), input_digest=input_digest, output_files=request.snapshot.files, - description=f"Run yapf on {pluralize(len(request.field_sets), 'file')}.", + description=f"Run yapf on {pluralize(len(request.snapshot.files), 'file')}.", level=LogLevel.DEBUG, ), ) @@ -70,6 +77,13 @@ async def yapf_fmt(request: YapfRequest, yapf: Yapf) -> FmtResult: return FmtResult.create(request, result, output_snapshot) +@rule(desc="Format with yapf", level=LogLevel.DEBUG) +async def yapf_fmt(request: YapfRequest, yapf: Yapf) -> FmtResult: + if yapf.skip: + return FmtResult.skip(formatter_name=request.name) + return await _run_yapf(request, yapf) + + def rules(): return [ *collect_rules(), From e0e61cf938e4cadde576e7dcfe03c54b71f92b42 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Wed, 17 Aug 2022 13:56:00 -0500 Subject: [PATCH 03/15] Have update-build-files use new helpers # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- .../pants/core/goals/update_build_files.py | 100 ++++-------------- 1 file changed, 19 insertions(+), 81 deletions(-) diff --git a/src/python/pants/core/goals/update_build_files.py b/src/python/pants/core/goals/update_build_files.py index 057e3b25669..30589588616 100644 --- a/src/python/pants/core/goals/update_build_files.py +++ b/src/python/pants/core/goals/update_build_files.py @@ -15,12 +15,14 @@ from colors import green, red +from pants.backend.build_files.fmt.black.register import BlackRequest +from pants.backend.build_files.fmt.yapf.register import YapfRequest +from pants.backend.python.lint.black.rules import _run_black from pants.backend.python.lint.black.subsystem import Black +from pants.backend.python.lint.yapf.rules import _run_yapf from pants.backend.python.lint.yapf.subsystem import Yapf from pants.backend.python.util_rules import pex -from pants.backend.python.util_rules.pex import PexRequest, VenvPex, VenvPexProcess from pants.base.specs import Specs -from pants.core.util_rules.config_files import ConfigFiles, ConfigFilesRequest from pants.engine.console import Console from pants.engine.engine_aware import EngineAwareParameter from pants.engine.fs import ( @@ -28,21 +30,19 @@ Digest, DigestContents, FileContent, - MergeDigests, PathGlobs, Paths, + Snapshot, SpecsPaths, Workspace, ) from pants.engine.goal import Goal, GoalSubsystem from pants.engine.internals.build_files import BuildFileOptions from pants.engine.internals.parser import ParseError -from pants.engine.process import ProcessResult from pants.engine.rules import Get, MultiGet, collect_rules, goal_rule, rule from pants.engine.target import RegisteredTargetTypes, TargetGenerator from pants.engine.unions import UnionMembership, UnionRule, union from pants.option.option_types import BoolOption, EnumOption -from pants.util.dirutil import recursive_dirname from pants.util.docutil import bin_name, doc_url from pants.util.frozendict import FrozenDict from pants.util.logging import LogLevel @@ -306,45 +306,16 @@ class FormatWithYapfRequest(RewrittenBuildFileRequest): async def format_build_file_with_yapf( request: FormatWithYapfRequest, yapf: Yapf ) -> RewrittenBuildFile: + input_snapshot = await Get(Snapshot, CreateDigest([request.to_file_content()])) yapf_ics = await Yapf._find_python_interpreter_constraints_from_lockfile(yapf) - yapf_pex_get = Get(VenvPex, PexRequest, yapf.to_pex_request(interpreter_constraints=yapf_ics)) - build_file_digest_get = Get(Digest, CreateDigest([request.to_file_content()])) - config_files_get = Get( - ConfigFiles, ConfigFilesRequest, yapf.config_request(recursive_dirname(request.path)) - ) - yapf_pex, build_file_digest, config_files = await MultiGet( - yapf_pex_get, build_file_digest_get, config_files_get - ) + result = await _run_yapf(YapfRequest(input_snapshot), yapf, yapf_ics) + output_content = await Get(DigestContents, Digest, result.output.digest) - input_digest = await Get( - Digest, MergeDigests((build_file_digest, config_files.snapshot.digest)) - ) + formatted_build_file_content = next(fc for fc in output_content if fc.path == request.path) + build_lines = tuple(formatted_build_file_content.content.decode("utf-8").splitlines()) + change_descriptions = ("Format with Yapf",) if result.did_change else () - argv = ["--in-place"] - if yapf.config: - argv.extend(["--config", yapf.config]) - argv.extend(yapf.args) - argv.append(request.path) - - yapf_result = await Get( - ProcessResult, - VenvPexProcess( - yapf_pex, - argv=argv, - input_digest=input_digest, - output_files=(request.path,), - description=f"Run Yapf on {request.path}.", - level=LogLevel.DEBUG, - ), - ) - - if yapf_result.output_digest == build_file_digest: - return RewrittenBuildFile(request.path, request.lines, change_descriptions=()) - - result_contents = await Get(DigestContents, Digest, yapf_result.output_digest) - assert len(result_contents) == 1 - result_lines = tuple(result_contents[0].content.decode("utf-8").splitlines()) - return RewrittenBuildFile(request.path, result_lines, change_descriptions=("Format with Yapf",)) + return RewrittenBuildFile(request.path, build_lines, change_descriptions=change_descriptions) # ------------------------------------------------------------------------------------------ @@ -360,49 +331,16 @@ class FormatWithBlackRequest(RewrittenBuildFileRequest): async def format_build_file_with_black( request: FormatWithBlackRequest, black: Black ) -> RewrittenBuildFile: + input_snapshot = await Get(Snapshot, CreateDigest([request.to_file_content()])) black_ics = await Black._find_python_interpreter_constraints_from_lockfile(black) - black_pex_get = Get( - VenvPex, PexRequest, black.to_pex_request(interpreter_constraints=black_ics) - ) - build_file_digest_get = Get(Digest, CreateDigest([request.to_file_content()])) - config_files_get = Get( - ConfigFiles, ConfigFilesRequest, black.config_request(recursive_dirname(request.path)) - ) - black_pex, build_file_digest, config_files = await MultiGet( - black_pex_get, build_file_digest_get, config_files_get - ) - - input_digest = await Get( - Digest, MergeDigests((build_file_digest, config_files.snapshot.digest)) - ) - - argv = [] - if black.config: - argv.extend(["--config", black.config]) - argv.extend(black.args) - argv.append(request.path) - - black_result = await Get( - ProcessResult, - VenvPexProcess( - black_pex, - argv=argv, - input_digest=input_digest, - output_files=(request.path,), - description=f"Run Black on {request.path}.", - level=LogLevel.DEBUG, - ), - ) + result = await _run_black(BlackRequest(input_snapshot), black, black_ics) + output_content = await Get(DigestContents, Digest, result.output.digest) - if black_result.output_digest == build_file_digest: - return RewrittenBuildFile(request.path, request.lines, change_descriptions=()) + formatted_build_file_content = next(fc for fc in output_content if fc.path == request.path) + build_lines = tuple(formatted_build_file_content.content.decode("utf-8").splitlines()) + change_descriptions = ("Format with Black",) if result.did_change else () - result_contents = await Get(DigestContents, Digest, black_result.output_digest) - assert len(result_contents) == 1 - result_lines = tuple(result_contents[0].content.decode("utf-8").splitlines()) - return RewrittenBuildFile( - request.path, result_lines, change_descriptions=("Format with Black",) - ) + return RewrittenBuildFile(request.path, build_lines, change_descriptions=change_descriptions) # ------------------------------------------------------------------------------------------ From 430682d940eb5bd89b55ead0de3d121aec9e8018 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Wed, 17 Aug 2022 13:56:12 -0500 Subject: [PATCH 04/15] Plumb new build formatting plugins # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- src/python/pants/backend/build_files/BUILD | 4 +++ .../pants/backend/build_files/__init__.py | 0 .../pants/backend/build_files/fmt/BUILD | 4 +++ .../pants/backend/build_files/fmt/__init__.py | 0 .../pants/backend/build_files/fmt/black/BUILD | 4 +++ .../backend/build_files/fmt/black/__init__.py | 0 .../backend/build_files/fmt/black/register.py | 28 +++++++++++++++++++ .../pants/backend/build_files/fmt/yapf/BUILD | 4 +++ .../backend/build_files/fmt/yapf/__init__.py | 0 .../backend/build_files/fmt/yapf/register.py | 28 +++++++++++++++++++ 10 files changed, 72 insertions(+) create mode 100644 src/python/pants/backend/build_files/BUILD create mode 100644 src/python/pants/backend/build_files/__init__.py create mode 100644 src/python/pants/backend/build_files/fmt/BUILD create mode 100644 src/python/pants/backend/build_files/fmt/__init__.py create mode 100644 src/python/pants/backend/build_files/fmt/black/BUILD create mode 100644 src/python/pants/backend/build_files/fmt/black/__init__.py create mode 100644 src/python/pants/backend/build_files/fmt/black/register.py create mode 100644 src/python/pants/backend/build_files/fmt/yapf/BUILD create mode 100644 src/python/pants/backend/build_files/fmt/yapf/__init__.py create mode 100644 src/python/pants/backend/build_files/fmt/yapf/register.py diff --git a/src/python/pants/backend/build_files/BUILD b/src/python/pants/backend/build_files/BUILD new file mode 100644 index 00000000000..95c6150585e --- /dev/null +++ b/src/python/pants/backend/build_files/BUILD @@ -0,0 +1,4 @@ +# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +python_sources() diff --git a/src/python/pants/backend/build_files/__init__.py b/src/python/pants/backend/build_files/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/python/pants/backend/build_files/fmt/BUILD b/src/python/pants/backend/build_files/fmt/BUILD new file mode 100644 index 00000000000..95c6150585e --- /dev/null +++ b/src/python/pants/backend/build_files/fmt/BUILD @@ -0,0 +1,4 @@ +# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +python_sources() diff --git a/src/python/pants/backend/build_files/fmt/__init__.py b/src/python/pants/backend/build_files/fmt/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/python/pants/backend/build_files/fmt/black/BUILD b/src/python/pants/backend/build_files/fmt/black/BUILD new file mode 100644 index 00000000000..95c6150585e --- /dev/null +++ b/src/python/pants/backend/build_files/fmt/black/BUILD @@ -0,0 +1,4 @@ +# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +python_sources() diff --git a/src/python/pants/backend/build_files/fmt/black/__init__.py b/src/python/pants/backend/build_files/fmt/black/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/python/pants/backend/build_files/fmt/black/register.py b/src/python/pants/backend/build_files/fmt/black/register.py new file mode 100644 index 00000000000..1a67a899ccb --- /dev/null +++ b/src/python/pants/backend/build_files/fmt/black/register.py @@ -0,0 +1,28 @@ +# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from pants.backend.python.lint.black import subsystem as black_subsystem +from pants.backend.python.lint.black.rules import _run_black +from pants.backend.python.lint.black.subsystem import Black +from pants.core.goals.fmt import FmtResult, _FmtBuildFilesRequest +from pants.engine.rules import collect_rules, rule +from pants.engine.unions import UnionRule +from pants.util.logging import LogLevel + + +class BlackRequest(_FmtBuildFilesRequest): + name = "black" + + +@rule(desc="Format with Black", level=LogLevel.DEBUG) +async def black_fmt(request: BlackRequest, black: Black) -> FmtResult: + black_ics = await Black._find_python_interpreter_constraints_from_lockfile(black) + return await _run_black(request, black, black_ics) + + +def rules(): + return [ + *collect_rules(), + UnionRule(_FmtBuildFilesRequest, BlackRequest), + *black_subsystem.rules(), + ] diff --git a/src/python/pants/backend/build_files/fmt/yapf/BUILD b/src/python/pants/backend/build_files/fmt/yapf/BUILD new file mode 100644 index 00000000000..95c6150585e --- /dev/null +++ b/src/python/pants/backend/build_files/fmt/yapf/BUILD @@ -0,0 +1,4 @@ +# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +python_sources() diff --git a/src/python/pants/backend/build_files/fmt/yapf/__init__.py b/src/python/pants/backend/build_files/fmt/yapf/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/python/pants/backend/build_files/fmt/yapf/register.py b/src/python/pants/backend/build_files/fmt/yapf/register.py new file mode 100644 index 00000000000..3cfb8e76ad2 --- /dev/null +++ b/src/python/pants/backend/build_files/fmt/yapf/register.py @@ -0,0 +1,28 @@ +# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from pants.backend.python.lint.yapf import subsystem as yapf_subsystem +from pants.backend.python.lint.yapf.rules import _run_yapf +from pants.backend.python.lint.yapf.subsystem import Yapf +from pants.core.goals.fmt import FmtResult, _FmtBuildFilesRequest +from pants.engine.rules import collect_rules, rule +from pants.engine.unions import UnionRule +from pants.util.logging import LogLevel + + +class YapfRequest(_FmtBuildFilesRequest): + name = "black" + + +@rule(desc="Format with Yapf", level=LogLevel.DEBUG) +async def yapf_fmt(request: YapfRequest, yapf: Yapf) -> FmtResult: + yapf_ics = await Yapf._find_python_interpreter_constraints_from_lockfile(yapf) + return await _run_yapf(request, yapf, yapf_ics) + + +def rules(): + return [ + *collect_rules(), + UnionRule(_FmtBuildFilesRequest, YapfRequest), + *yapf_subsystem.rules(), + ] From 14adcfe7d83b1c26ef2769c8e3090c4775971c68 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Wed, 17 Aug 2022 13:56:23 -0500 Subject: [PATCH 05/15] Dogfood! Yum! # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- pants.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pants.toml b/pants.toml index 81976d954f6..ec99d609453 100644 --- a/pants.toml +++ b/pants.toml @@ -4,6 +4,7 @@ print_stacktrace = true # Enable our custom loose-source plugins. pythonpath = ["%(buildroot)s/pants-plugins"] backend_packages.add = [ + "pants.backend.build_files.fmt.black", "pants.backend.python", "pants.backend.experimental.python.lint.autoflake", "pants.backend.explorer", From efdf6579711b110bfef1092acd0ebd4289485824 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Wed, 17 Aug 2022 13:56:29 -0500 Subject: [PATCH 06/15] Update CI # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- .github/workflows/test-cron.yaml | 4 +--- .github/workflows/test.yaml | 4 +--- build-support/bin/generate_github_workflows.py | 1 - 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test-cron.yaml b/.github/workflows/test-cron.yaml index b257b17176f..a6b34fe9cd4 100644 --- a/.github/workflows/test-cron.yaml +++ b/.github/workflows/test-cron.yaml @@ -349,9 +349,7 @@ jobs: ' - name: Lint - run: './pants update-build-files --check :: - - ./pants lint check ''**'' + run: './pants lint check ''**'' ' - continue-on-error: true diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7ff875183f9..3455d2be3c6 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -815,9 +815,7 @@ jobs: ' - name: Lint - run: './pants update-build-files --check :: - - ./pants lint check ''**'' + run: './pants lint check ''**'' ' - continue-on-error: true diff --git a/build-support/bin/generate_github_workflows.py b/build-support/bin/generate_github_workflows.py index db16151d684..413263f676e 100644 --- a/build-support/bin/generate_github_workflows.py +++ b/build-support/bin/generate_github_workflows.py @@ -767,7 +767,6 @@ def test_workflow_jobs(python_versions: list[str], *, cron: bool) -> Jobs: { "name": "Lint", "run": ( - "./pants update-build-files --check ::\n" # Note: we use `**` rather than `::` because regex-lint. "./pants lint check '**'\n" ), From a0a6ce95bb096a0c49378d933ed81e7023254c00 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Wed, 17 Aug 2022 14:42:05 -0500 Subject: [PATCH 07/15] feedback --- .github/workflows/test-cron.yaml | 2 +- .github/workflows/test.yaml | 2 +- build-support/bin/generate_github_workflows.py | 5 +---- src/python/pants/bin/BUILD | 2 ++ 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-cron.yaml b/.github/workflows/test-cron.yaml index a6b34fe9cd4..a7fa6fe88c7 100644 --- a/.github/workflows/test-cron.yaml +++ b/.github/workflows/test-cron.yaml @@ -349,7 +349,7 @@ jobs: ' - name: Lint - run: './pants lint check ''**'' + run: './pants lint check :: ' - continue-on-error: true diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3455d2be3c6..4b9eddbf67d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -815,7 +815,7 @@ jobs: ' - name: Lint - run: './pants lint check ''**'' + run: './pants lint check :: ' - continue-on-error: true diff --git a/build-support/bin/generate_github_workflows.py b/build-support/bin/generate_github_workflows.py index 413263f676e..851bb24142c 100644 --- a/build-support/bin/generate_github_workflows.py +++ b/build-support/bin/generate_github_workflows.py @@ -766,10 +766,7 @@ def test_workflow_jobs(python_versions: list[str], *, cron: bool) -> Jobs: setup_toolchain_auth(), { "name": "Lint", - "run": ( - # Note: we use `**` rather than `::` because regex-lint. - "./pants lint check '**'\n" - ), + "run": "./pants lint check ::\n", }, linux_x86_64_helper.upload_log_artifacts(name="lint"), ], diff --git a/src/python/pants/bin/BUILD b/src/python/pants/bin/BUILD index 74ecd622770..c5446843f7b 100644 --- a/src/python/pants/bin/BUILD +++ b/src/python/pants/bin/BUILD @@ -14,6 +14,8 @@ target( name="plugins", dependencies=[ "src/python/pants/backend/awslambda/python", + "src/python/pants/backend/build_files/fmt/black", + "src/python/pants/backend/build_files/fmt/yapf", "src/python/pants/backend/codegen/protobuf/lint/buf", "src/python/pants/backend/codegen/protobuf/python", "src/python/pants/backend/codegen/thrift/apache/python", From 7c8d0aae9f4ecff6931589ef3d5bf515b35a7d46 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Wed, 17 Aug 2022 14:51:26 -0500 Subject: [PATCH 08/15] whitespace removal # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- docs/markdown/Releases/upgrade-tips.md | 14 ++--- docs/markdown/Using Pants/concepts/targets.md | 58 +++++++++---------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/markdown/Releases/upgrade-tips.md b/docs/markdown/Releases/upgrade-tips.md index 8c5b790e512..ff80ee54acd 100644 --- a/docs/markdown/Releases/upgrade-tips.md +++ b/docs/markdown/Releases/upgrade-tips.md @@ -7,9 +7,9 @@ createdAt: "2020-05-16T22:53:24.499Z" updatedAt: "2022-01-13T04:03:33.172Z" --- > 📘 Reminder: change the `pants_version` to upgrade -> +> > Change the `pants_version` option in the `[GLOBAL]` scope in your pants.toml to upgrade. -> +> > You can see all releases at . Upgrade one minor release at a time @@ -17,14 +17,14 @@ Upgrade one minor release at a time Per our [Deprecation policy](doc:deprecation-policy), deprecations must last a minimum of one minor release. For example, something may be deprecated in 2.1.0 and then removed in 2.2.0. -This means that it is helpful to upgrade one minor release at a time so that you can see all deprecation warnings. +This means that it is helpful to upgrade one minor release at a time so that you can see all deprecation warnings. You do not need to land every upgrade into your organization—often, you will want to upgrade your organization multiple versions at a time, e.g. 2.1.0 to 2.4.0. But, when you are working on the upgrade locally, it is helpful to iterate one version at a time. First, see if Pants can automatically fix any safe deprecations for you: ```bash -# You may want to use `--no-fmt` if your BUILD files are +# You may want to use `--no-fmt` if your BUILD files are # not already formatted by Black. ❯ ./pants update-build-files --no-fmt :: ``` @@ -46,9 +46,9 @@ Then, see if there are any remaining deprecation warnings: It is also helpful to spot-check that your main commands like `lint`, `package`, and `test` still work by running on a single target. > 📘 Use dev releases for the newest -> -> As described in our [Release strategy](doc:release-strategy), we make weekly dev releases with all the latest features and bug fixes we've been working on. While dev releases are less stable, they mean you get access to improvements sooner. -> +> +> As described in our [Release strategy](doc:release-strategy), we make weekly dev releases with all the latest features and bug fixes we've been working on. While dev releases are less stable, they mean you get access to improvements sooner. +> > If you encounter any blocking issues, you can easily roll back to a prior version by changing the `pants_version` option. (Please let us know the issue by opening a [GitHub issue](https://github.com/pantsbuild/pants/issues) or messaging us on [Slack](doc:community)). Ignore deprecation messages with `ignore_warnings` diff --git a/docs/markdown/Using Pants/concepts/targets.md b/docs/markdown/Using Pants/concepts/targets.md index 9a2dd212065..97b14ac32d2 100644 --- a/docs/markdown/Using Pants/concepts/targets.md +++ b/docs/markdown/Using Pants/concepts/targets.md @@ -55,18 +55,18 @@ Addresses are used in the `dependencies` field to depend on other targets. Addre (Both "generated targets" and "parametrized targets" have a variant of this syntax; see the below sections.) > 📘 Default for the `name` field -> +> > The `name` field defaults to the directory name. So, this target has the address `helloworld/greet:greet`. -> +> > ```python > # helloworld/greet/BUILD > python_sources() > ``` -> +> > You can refer to this target with either `helloworld/greet:greet` or the abbreviated form `helloworld/greet`. > 📘 Use `//:tgt` for the root of your repository -> +> > Addressed defined in the `BUILD` file at the root of your repository are prefixed with `//`, e.g. `//:my_tgt`. `source` and `sources` field @@ -88,26 +88,26 @@ python_tests( ``` > 🚧 Be careful with overlapping `source` fields -> -> It's legal to include the same file in the `source` / `sources` field for multiple targets. -> +> +> It's legal to include the same file in the `source` / `sources` field for multiple targets. +> > When would you do this? Sometimes you may have conflicting metadata for the same source file, such as wanting to check that a Shell test works with multiple shells. Normally, you should prefer Pants's `parametrize` mechanism to do this. See the below section "Parametrizing Targets". -> +> > Often, however, it is not intentional when multiple targets own the same file. For example, this often happens when using `**` globs, like this: -> +> > ```python > # project/BUILD > python_sources(sources=["**/*.py"]) -> +> > # project/subdir/BUILD > python_sources(sources=["**/*.py"]) > ``` -> -> Including the same file in the `source` / `sources` field for multiple targets can result in two confusing behaviors: -> +> +> Including the same file in the `source` / `sources` field for multiple targets can result in two confusing behaviors: +> > - File arguments will run over all owning targets, e.g. `./pants test path/to/test.ext` would run both test targets as two separate subprocesses, even though you might only expect a single subprocess. > - Pants will sometimes no longer be able to infer dependencies on this file because it cannot disambiguate which of the targets you want to use. You must use explicit dependencies instead. (For some blessed fields, like the `resolve` field, if the targets have different values, then there will not be ambiguity.) -> +> > You can run `./pants list path/to/file.ext` to see all "owning" targets to check if >1 target has the file in its `source` field. `dependencies` field @@ -117,7 +117,7 @@ A target's dependencies determines which other first-party code and third-party Usually, you leave off the `dependencies` field thanks to _dependency inference_. Pants will read your import statements and map those imports back to your first-party code and your third-party requirements. You can run `./pants dependencies path/to:target` to see what dependencies Pants infers. -However, dependency inference cannot infer everything, such as dependencies on `resource` and `file` targets. +However, dependency inference cannot infer everything, such as dependencies on `resource` and `file` targets. To add an explicit dependency, add the target's address to the `dependencies` field. This augments any dependencies that were inferred. @@ -134,24 +134,24 @@ python_sources( You only need to declare direct dependencies. Pants will pull in _transitive dependencies_—i.e. the dependencies of your dependencies—for you. > 📘 Relative addresses, `:tgt` -> -> When depending on a target defined in the same BUILD file, you can simply use `:tgt_name`, rather than `helloworld/greet:tgt_name`, for example. -> +> +> When depending on a target defined in the same BUILD file, you can simply use `:tgt_name`, rather than `helloworld/greet:tgt_name`, for example. +> > Addresses for generated targets also support relative addresses in the `dependencies` field, as explained in the "Target Generation" section below. > 📘 Ignore dependencies with `!` and `!!` -> +> > If you don't like that Pants inferred a certain dependency—as reported by [`./pants dependencies path/to:tgt`](doc:project-introspection)—tell Pants to ignore it with `!`: -> +> > ```python > python_sources( > name="lib", > dependencies=["!3rdparty/python:numpy"], > ) > ``` -> -> You can use the prefix `!!` to transitively exclude a dependency, meaning that even if a target's dependencies include the bad dependency, the final result will not include the value. -> +> +> You can use the prefix `!!` to transitively exclude a dependency, meaning that even if a target's dependencies include the bad dependency, the final result will not include the value. +> > Transitive excludes can only be used in target types that conventionally are not dependend upon by other targets, such as `pex_binary` and `python_test` / `python_tests`. This is meant to limit confusion, as using `!!` in something like a `python_source` / `python_sources` target could result in surprising behavior for everything that depends on it. (Pants will print a helpful error when using `!!` when it's not legal.) Field default values @@ -273,13 +273,13 @@ Run [`./pants list dir:`](doc:project-introspection) in the directory of the tar You can use the address for the target generator as an alias for all of its generated targets. For example, if you have the `files` target `assets:logos`, adding `dependencies=["assets:logos"]`to another target will add a dependency on each generated `file` target. Likewise, if you have a `python_tests` target `project:tests`, then `./pants test project:tests` will run on each generated `python_test` target. > 📘 Tip: one BUILD file per directory -> +> > Target generation means that it is technically possible to put everything in a single BUILD file. -> -> However, we've found that it usually scales much better to use a single BUILD file per directory. Even if you start with using the defaults for everything, projects usually need to change some metadata over time, like adding a `timeout` to a test file or adding `dependencies` on resources. -> +> +> However, we've found that it usually scales much better to use a single BUILD file per directory. Even if you start with using the defaults for everything, projects usually need to change some metadata over time, like adding a `timeout` to a test file or adding `dependencies` on resources. +> > It's useful for metadata to be as fine-grained as feasible, such as by using the `overrides` field to only change the files you need to. Fine-grained metadata is key to having smaller cache keys (resulting in more cache hits), and allows you to more accurately reflect the status of your project. We have found that using one BUILD file per directory encourages fine-grained metadata by defining the metadata adjacent to where the code lives. -> +> > [`./pants tailor ::`](doc:initial-configuration#5-generate-build-files) will automatically create targets that only apply metadata for the directory. Parametrizing targets @@ -347,7 +347,7 @@ Parametrization can be combined with target generation. The `@key=value` will be # example:tests@shell=zsh # # Generally, you can still use `example:tests` -# without the `@` suffix as an alias to all the +# without the `@` suffix as an alias to all the # created targets. shunit2_tests( From cadeb4a633d7a0cba429279bc2f41429f9b42b6c Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Wed, 17 Aug 2022 14:51:40 -0500 Subject: [PATCH 09/15] docs changes # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- docs/markdown/Releases/upgrade-tips.md | 3 +-- docs/markdown/Using Pants/concepts/targets.md | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/markdown/Releases/upgrade-tips.md b/docs/markdown/Releases/upgrade-tips.md index ff80ee54acd..7fd977d4fcc 100644 --- a/docs/markdown/Releases/upgrade-tips.md +++ b/docs/markdown/Releases/upgrade-tips.md @@ -24,8 +24,7 @@ You do not need to land every upgrade into your organization—often, you will w First, see if Pants can automatically fix any safe deprecations for you: ```bash -# You may want to use `--no-fmt` if your BUILD files are -# not already formatted by Black. +# You may want to use `--no-fmt` if your BUILD files are already formatted. ❯ ./pants update-build-files --no-fmt :: ``` diff --git a/docs/markdown/Using Pants/concepts/targets.md b/docs/markdown/Using Pants/concepts/targets.md index 97b14ac32d2..c70f2257384 100644 --- a/docs/markdown/Using Pants/concepts/targets.md +++ b/docs/markdown/Using Pants/concepts/targets.md @@ -43,7 +43,9 @@ Each target type has different _fields_, or individual metadata values. Run `./p All target types have a `name` field, which is used to identify the target. Target names must be unique within a directory. -Use [`./pants tailor ::`](doc:initial-configuration#5-generate-build-files) to automate generating BUILD files, and [`./pants update-build-files ::`](doc:reference-update-build-files) to reformat them (using `black`, [by default](doc:reference-update-build-files#section-formatter)). +Use [`./pants tailor ::`](doc:initial-configuration#5-generate-build-files) to automate generating BUILD files. +You can autoformat build files by opting into one of the plugins under `pants.backend.build_files.fmt` and +running `./pants fmt **/BUILD` or `./pants fmt ::` (which formats everything). Target addresses ================ From 621530eb015c89d53fa29f8e24c5e3b1d1c1613c Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Wed, 17 Aug 2022 15:32:57 -0500 Subject: [PATCH 10/15] Add tests! # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- .../pants/backend/build_files/fmt/black/BUILD | 2 + .../build_files/fmt/black/integration_test.py | 130 ++++++++++++++++++ .../pants/backend/build_files/fmt/yapf/BUILD | 2 + .../build_files/fmt/yapf/integration_test.py | 120 ++++++++++++++++ .../backend/build_files/fmt/yapf/register.py | 2 +- 5 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 src/python/pants/backend/build_files/fmt/black/integration_test.py create mode 100644 src/python/pants/backend/build_files/fmt/yapf/integration_test.py diff --git a/src/python/pants/backend/build_files/fmt/black/BUILD b/src/python/pants/backend/build_files/fmt/black/BUILD index 95c6150585e..27e5628650a 100644 --- a/src/python/pants/backend/build_files/fmt/black/BUILD +++ b/src/python/pants/backend/build_files/fmt/black/BUILD @@ -2,3 +2,5 @@ # Licensed under the Apache License, Version 2.0 (see LICENSE). python_sources() + +python_tests(name="tests") diff --git a/src/python/pants/backend/build_files/fmt/black/integration_test.py b/src/python/pants/backend/build_files/fmt/black/integration_test.py new file mode 100644 index 00000000000..5a09c0fd478 --- /dev/null +++ b/src/python/pants/backend/build_files/fmt/black/integration_test.py @@ -0,0 +1,130 @@ +# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import annotations + +import pytest + +from pants.backend.build_files.fmt.black.register import BlackRequest +from pants.backend.build_files.fmt.black.register import rules as black_build_rules +from pants.backend.python.lint.black.rules import rules as black_fmt_rules +from pants.backend.python.lint.black.subsystem import Black +from pants.backend.python.lint.black.subsystem import rules as black_subsystem_rules +from pants.backend.python.target_types import PythonSourcesGeneratorTarget +from pants.core.goals.fmt import FmtResult +from pants.core.util_rules import config_files +from pants.engine.fs import CreateDigest, Digest, FileContent, PathGlobs +from pants.engine.internals.native_engine import Snapshot +from pants.testutil.python_interpreter_selection import all_major_minor_python_versions +from pants.testutil.rule_runner import QueryRule, RuleRunner + + +@pytest.fixture +def rule_runner() -> RuleRunner: + return RuleRunner( + rules=[ + *black_build_rules(), + *black_fmt_rules(), + *black_subsystem_rules(), + *config_files.rules(), + QueryRule(FmtResult, (BlackRequest,)), + ], + target_types=[PythonSourcesGeneratorTarget], + ) + + +def get_snapshot(rule_runner: RuleRunner, build_files: dict[str, str]) -> Snapshot: + files = [FileContent(path, content.encode()) for path, content in build_files.items()] + digest = rule_runner.request(Digest, [CreateDigest(files)]) + return rule_runner.request(Snapshot, [digest]) + + +def run_black(rule_runner: RuleRunner, *, extra_args: list[str] | None = None) -> FmtResult: + rule_runner.set_options( + ["--backend-packages=pants.backend.build_files.fmt.black", *(extra_args or ())], + # We propagate LANG and LC_ALL to satisfy click, which black depends upon. Without this we + # see something like the following in CI: + # + # RuntimeError: Click will abort further execution because Python was configured to use + # ASCII as encoding for the environment. Consult + # https://click.palletsprojects.com/unicode-support/ for mitigation steps. + # + # This system supports the C.UTF-8 locale which is recommended. You might be able to + # resolve your issue by exporting the following environment variables: + # + # export LC_ALL=C.UTF-8 + # export LANG=C.UTF-8 + # + env_inherit={"PATH", "PYENV_ROOT", "HOME", "LANG", "LC_ALL"}, + ) + snapshot = rule_runner.request(Snapshot, [PathGlobs(["**/BUILD"])]) + fmt_result = rule_runner.request(FmtResult, [BlackRequest(snapshot)]) + return fmt_result + + +@pytest.mark.platform_specific_behavior +@pytest.mark.parametrize( + "major_minor_interpreter", + all_major_minor_python_versions(Black.default_interpreter_constraints), +) +def test_passing(rule_runner: RuleRunner, major_minor_interpreter: str) -> None: + rule_runner.write_files({"BUILD": 'python_sources(name="t")\n'}) + interpreter_constraint = ( + ">=3.6.2,<3.7" if major_minor_interpreter == "3.6" else f"=={major_minor_interpreter}.*" + ) + fmt_result = run_black( + rule_runner, + extra_args=[f"--black-interpreter-constraints=['{interpreter_constraint}']"], + ) + assert "1 file left unchanged" in fmt_result.stderr + assert fmt_result.output == get_snapshot(rule_runner, {"BUILD": 'python_sources(name="t")\n'}) + assert fmt_result.did_change is False + + +def test_failing(rule_runner: RuleRunner) -> None: + rule_runner.write_files({"BUILD": "python_sources(name='t')\n"}) + fmt_result = run_black(rule_runner) + assert "1 file reformatted" in fmt_result.stderr + assert fmt_result.output == get_snapshot(rule_runner, {"BUILD": 'python_sources(name="t")\n'}) + assert fmt_result.did_change is True + + +def test_multiple_files(rule_runner: RuleRunner) -> None: + rule_runner.write_files( + { + "good/BUILD": 'python_sources(name="t")\n', + "bad/BUILD": "python_sources(name='t')\n", + } + ) + fmt_result = run_black(rule_runner) + assert "1 file reformatted, 1 file left unchanged" in fmt_result.stderr + assert fmt_result.output == get_snapshot( + rule_runner, + {"good/BUILD": 'python_sources(name="t")\n', "bad/BUILD": 'python_sources(name="t")\n'}, + ) + assert fmt_result.did_change is True + + +@pytest.mark.parametrize( + "config_path,extra_args", + (["pyproject.toml", []], ["custom_config.toml", ["--black-config=custom_config.toml"]]), +) +def test_config_file(rule_runner: RuleRunner, config_path: str, extra_args: list[str]) -> None: + rule_runner.write_files( + { + "BUILD": "python_sources(name='t')\n", + config_path: "[tool.black]\nskip-string-normalization = 'true'\n", + } + ) + fmt_result = run_black(rule_runner, extra_args=extra_args) + assert "1 file left unchanged" in fmt_result.stderr + assert fmt_result.output == get_snapshot(rule_runner, {"BUILD": "python_sources(name='t')\n"}) + assert fmt_result.did_change is False + + +def test_passthrough_args(rule_runner: RuleRunner) -> None: + rule_runner.write_files({"BUILD": "python_sources(name='t')\n"}) + fmt_result = run_black(rule_runner, extra_args=["--black-args='--skip-string-normalization'"]) + assert "1 file left unchanged" in fmt_result.stderr + assert fmt_result.output == get_snapshot(rule_runner, {"BUILD": "python_sources(name='t')\n"}) + assert fmt_result.did_change is False diff --git a/src/python/pants/backend/build_files/fmt/yapf/BUILD b/src/python/pants/backend/build_files/fmt/yapf/BUILD index 95c6150585e..27e5628650a 100644 --- a/src/python/pants/backend/build_files/fmt/yapf/BUILD +++ b/src/python/pants/backend/build_files/fmt/yapf/BUILD @@ -2,3 +2,5 @@ # Licensed under the Apache License, Version 2.0 (see LICENSE). python_sources() + +python_tests(name="tests") diff --git a/src/python/pants/backend/build_files/fmt/yapf/integration_test.py b/src/python/pants/backend/build_files/fmt/yapf/integration_test.py new file mode 100644 index 00000000000..93836f3fdd5 --- /dev/null +++ b/src/python/pants/backend/build_files/fmt/yapf/integration_test.py @@ -0,0 +1,120 @@ +# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import annotations + +import pytest + +from pants.backend.build_files.fmt.yapf.register import YapfRequest +from pants.backend.build_files.fmt.yapf.register import rules as yapf_build_rules +from pants.backend.python.lint.yapf.rules import rules as yapf_fmt_rules +from pants.backend.python.lint.yapf.subsystem import Yapf +from pants.backend.python.lint.yapf.subsystem import rules as yapf_subsystem_rules +from pants.backend.python.target_types import PythonSourcesGeneratorTarget +from pants.core.goals.fmt import FmtResult +from pants.core.util_rules import config_files +from pants.engine.fs import CreateDigest, Digest, FileContent, PathGlobs +from pants.engine.internals.native_engine import Snapshot +from pants.testutil.python_interpreter_selection import all_major_minor_python_versions +from pants.testutil.rule_runner import QueryRule, RuleRunner + + +@pytest.fixture +def rule_runner() -> RuleRunner: + return RuleRunner( + rules=[ + *yapf_build_rules(), + *yapf_fmt_rules(), + *yapf_subsystem_rules(), + *config_files.rules(), + QueryRule(FmtResult, (YapfRequest,)), + ], + target_types=[PythonSourcesGeneratorTarget], + ) + + +def get_snapshot(rule_runner: RuleRunner, build_files: dict[str, str]) -> Snapshot: + files = [FileContent(path, content.encode()) for path, content in build_files.items()] + digest = rule_runner.request(Digest, [CreateDigest(files)]) + return rule_runner.request(Snapshot, [digest]) + + +def run_yapf(rule_runner: RuleRunner, *, extra_args: list[str] | None = None) -> FmtResult: + rule_runner.set_options( + ["--backend-packages=pants.backend.build_files.fmt.yapf", *(extra_args or ())], + env_inherit={"PATH", "PYENV_ROOT", "HOME"}, + ) + snapshot = rule_runner.request(Snapshot, [PathGlobs(["**/BUILD"])]) + fmt_result = rule_runner.request(FmtResult, [YapfRequest(snapshot)]) + return fmt_result + + +@pytest.mark.platform_specific_behavior +@pytest.mark.parametrize( + "major_minor_interpreter", + all_major_minor_python_versions(Yapf.default_interpreter_constraints), +) +def test_passing(rule_runner: RuleRunner, major_minor_interpreter: str) -> None: + rule_runner.write_files({"BUILD": 'python_sources(name="t")\n'}) + interpreter_constraint = ( + ">=3.6.2,<3.7" if major_minor_interpreter == "3.6" else f"=={major_minor_interpreter}.*" + ) + fmt_result = run_yapf( + rule_runner, + extra_args=[f"--yapf-interpreter-constraints=['{interpreter_constraint}']"], + ) + assert fmt_result.output == get_snapshot(rule_runner, {"BUILD": 'python_sources(name="t")\n'}) + assert fmt_result.did_change is False + + +def test_failing(rule_runner: RuleRunner) -> None: + rule_runner.write_files({"BUILD": 'python_sources(name = "t")\n'}) + fmt_result = run_yapf(rule_runner) + assert fmt_result.output == get_snapshot(rule_runner, {"BUILD": 'python_sources(name="t")\n'}) + assert fmt_result.did_change is True + + +def test_multiple_files(rule_runner: RuleRunner) -> None: + rule_runner.write_files( + { + "good/BUILD": 'python_sources(name="t")\n', + "bad/BUILD": 'python_sources(name = "t")\n', + } + ) + fmt_result = run_yapf(rule_runner) + assert fmt_result.output == get_snapshot( + rule_runner, + {"good/BUILD": 'python_sources(name="t")\n', "bad/BUILD": 'python_sources(name="t")\n'}, + ) + assert fmt_result.did_change is True + + +@pytest.mark.parametrize( + "path,section,extra_args", + ( + (".style.yapf", "style", []), + ("custom.style", "style", ["--yapf-config=custom.style"]), + ), +) +def test_config_file( + rule_runner: RuleRunner, path: str, section: str, extra_args: list[str] +) -> None: + rule_runner.write_files( + { + "BUILD": 'python_sources(name = "t")\n', + path: f"[{section}]\nspaces_around_default_or_named_assign = True\n", + } + ) + fmt_result = run_yapf(rule_runner, extra_args=extra_args) + assert fmt_result.output == get_snapshot(rule_runner, {"BUILD": 'python_sources(name = "t")\n'}) + assert fmt_result.did_change is False + + +def test_passthrough_args(rule_runner: RuleRunner) -> None: + rule_runner.write_files({"BUILD": 'python_sources(name = "t")\n'}) + fmt_result = run_yapf( + rule_runner, + extra_args=["--yapf-args=--style='{spaces_around_default_or_named_assign: True}'"], + ) + assert fmt_result.output == get_snapshot(rule_runner, {"BUILD": 'python_sources(name = "t")\n'}) + assert fmt_result.did_change is False diff --git a/src/python/pants/backend/build_files/fmt/yapf/register.py b/src/python/pants/backend/build_files/fmt/yapf/register.py index 3cfb8e76ad2..2ae19be5dbe 100644 --- a/src/python/pants/backend/build_files/fmt/yapf/register.py +++ b/src/python/pants/backend/build_files/fmt/yapf/register.py @@ -11,7 +11,7 @@ class YapfRequest(_FmtBuildFilesRequest): - name = "black" + name = "yapf" @rule(desc="Format with Yapf", level=LogLevel.DEBUG) From d2c8dc8caa7567d0b81df6b3efe4951c7b5e94e8 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Mon, 22 Aug 2022 10:34:11 -0500 Subject: [PATCH 11/15] some improvements # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- build-support/bin/generate_docs.py | 2 ++ .../build_files/fmt/yapf/integration_test.py | 19 ++++++------------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/build-support/bin/generate_docs.py b/build-support/bin/generate_docs.py index 23d21c894b3..e5977175e0f 100644 --- a/build-support/bin/generate_docs.py +++ b/build-support/bin/generate_docs.py @@ -232,6 +232,8 @@ def create_parser() -> argparse.ArgumentParser: def run_pants_help_all() -> dict[str, Any]: # List all (stable enough) backends here. backends = [ + "pants.backend.build_files.fmt.black", + "pants.backend.build_files.fmt.yapf", "pants.backend.awslambda.python", "pants.backend.codegen.protobuf.lint.buf", "pants.backend.codegen.protobuf.python", diff --git a/src/python/pants/backend/build_files/fmt/yapf/integration_test.py b/src/python/pants/backend/build_files/fmt/yapf/integration_test.py index 93836f3fdd5..b010ee3a364 100644 --- a/src/python/pants/backend/build_files/fmt/yapf/integration_test.py +++ b/src/python/pants/backend/build_files/fmt/yapf/integration_test.py @@ -13,7 +13,7 @@ from pants.backend.python.target_types import PythonSourcesGeneratorTarget from pants.core.goals.fmt import FmtResult from pants.core.util_rules import config_files -from pants.engine.fs import CreateDigest, Digest, FileContent, PathGlobs +from pants.engine.fs import PathGlobs from pants.engine.internals.native_engine import Snapshot from pants.testutil.python_interpreter_selection import all_major_minor_python_versions from pants.testutil.rule_runner import QueryRule, RuleRunner @@ -33,12 +33,6 @@ def rule_runner() -> RuleRunner: ) -def get_snapshot(rule_runner: RuleRunner, build_files: dict[str, str]) -> Snapshot: - files = [FileContent(path, content.encode()) for path, content in build_files.items()] - digest = rule_runner.request(Digest, [CreateDigest(files)]) - return rule_runner.request(Snapshot, [digest]) - - def run_yapf(rule_runner: RuleRunner, *, extra_args: list[str] | None = None) -> FmtResult: rule_runner.set_options( ["--backend-packages=pants.backend.build_files.fmt.yapf", *(extra_args or ())], @@ -63,14 +57,14 @@ def test_passing(rule_runner: RuleRunner, major_minor_interpreter: str) -> None: rule_runner, extra_args=[f"--yapf-interpreter-constraints=['{interpreter_constraint}']"], ) - assert fmt_result.output == get_snapshot(rule_runner, {"BUILD": 'python_sources(name="t")\n'}) + assert fmt_result.output == rule_runner.make_snapshot({"BUILD": 'python_sources(name="t")\n'}) assert fmt_result.did_change is False def test_failing(rule_runner: RuleRunner) -> None: rule_runner.write_files({"BUILD": 'python_sources(name = "t")\n'}) fmt_result = run_yapf(rule_runner) - assert fmt_result.output == get_snapshot(rule_runner, {"BUILD": 'python_sources(name="t")\n'}) + assert fmt_result.output == rule_runner.make_snapshot({"BUILD": 'python_sources(name="t")\n'}) assert fmt_result.did_change is True @@ -82,8 +76,7 @@ def test_multiple_files(rule_runner: RuleRunner) -> None: } ) fmt_result = run_yapf(rule_runner) - assert fmt_result.output == get_snapshot( - rule_runner, + assert fmt_result.output == rule_runner.make_snapshot( {"good/BUILD": 'python_sources(name="t")\n', "bad/BUILD": 'python_sources(name="t")\n'}, ) assert fmt_result.did_change is True @@ -106,7 +99,7 @@ def test_config_file( } ) fmt_result = run_yapf(rule_runner, extra_args=extra_args) - assert fmt_result.output == get_snapshot(rule_runner, {"BUILD": 'python_sources(name = "t")\n'}) + assert fmt_result.output == rule_runner.make_snapshot({"BUILD": 'python_sources(name = "t")\n'}) assert fmt_result.did_change is False @@ -116,5 +109,5 @@ def test_passthrough_args(rule_runner: RuleRunner) -> None: rule_runner, extra_args=["--yapf-args=--style='{spaces_around_default_or_named_assign: True}'"], ) - assert fmt_result.output == get_snapshot(rule_runner, {"BUILD": 'python_sources(name = "t")\n'}) + assert fmt_result.output == rule_runner.make_snapshot({"BUILD": 'python_sources(name = "t")\n'}) assert fmt_result.did_change is False From 5aef014826a785b027aa9e316f91e3f53357bfaf Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Mon, 22 Aug 2022 12:00:51 -0500 Subject: [PATCH 12/15] comments # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] --- docs/markdown/Releases/upgrade-tips.md | 2 +- docs/markdown/Using Pants/concepts/targets.md | 4 +--- pants.toml | 4 ++++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/markdown/Releases/upgrade-tips.md b/docs/markdown/Releases/upgrade-tips.md index 7fd977d4fcc..ed8351e2eb8 100644 --- a/docs/markdown/Releases/upgrade-tips.md +++ b/docs/markdown/Releases/upgrade-tips.md @@ -24,7 +24,7 @@ You do not need to land every upgrade into your organization—often, you will w First, see if Pants can automatically fix any safe deprecations for you: ```bash -# You may want to use `--no-fmt` if your BUILD files are already formatted. +# To avoid unrelated formatting changes, you may want to use `--no-fmt`. ❯ ./pants update-build-files --no-fmt :: ``` diff --git a/docs/markdown/Using Pants/concepts/targets.md b/docs/markdown/Using Pants/concepts/targets.md index c70f2257384..ad96491d564 100644 --- a/docs/markdown/Using Pants/concepts/targets.md +++ b/docs/markdown/Using Pants/concepts/targets.md @@ -43,9 +43,7 @@ Each target type has different _fields_, or individual metadata values. Run `./p All target types have a `name` field, which is used to identify the target. Target names must be unique within a directory. -Use [`./pants tailor ::`](doc:initial-configuration#5-generate-build-files) to automate generating BUILD files. -You can autoformat build files by opting into one of the plugins under `pants.backend.build_files.fmt` and -running `./pants fmt **/BUILD` or `./pants fmt ::` (which formats everything). +You can autoformat `BUILD` files by enabling a `BUILD` file formatter by adding it to `[GLOBAL].backend_packages` in `pants.toml` (such as `pants.backend.build_files.fmt.black` [or others](https://www.pantsbuild.org/v2.13/docs/enabling-backends)). Then to format, run `./pants fmt '**/BUILD'` or `./pants fmt ::` (formats everything). Target addresses ================ diff --git a/pants.toml b/pants.toml index ec99d609453..039d189528d 100644 --- a/pants.toml +++ b/pants.toml @@ -110,6 +110,10 @@ ignore_adding_targets = [ "src/python/pants/backend/terraform:hcl2_parser0", ] +[update-build-files] +# We use `pants.backend.build_files.fmt.black` +fmt = false + [pex] venv_use_symlinks = true From 17349435d11c113f629498fb98ea46dcd01ec821 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Mon, 22 Aug 2022 12:06:04 -0500 Subject: [PATCH 13/15] Update docs/markdown/Using Pants/concepts/targets.md Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> --- docs/markdown/Using Pants/concepts/targets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/markdown/Using Pants/concepts/targets.md b/docs/markdown/Using Pants/concepts/targets.md index ad96491d564..df528b78bf8 100644 --- a/docs/markdown/Using Pants/concepts/targets.md +++ b/docs/markdown/Using Pants/concepts/targets.md @@ -43,7 +43,7 @@ Each target type has different _fields_, or individual metadata values. Run `./p All target types have a `name` field, which is used to identify the target. Target names must be unique within a directory. -You can autoformat `BUILD` files by enabling a `BUILD` file formatter by adding it to `[GLOBAL].backend_packages` in `pants.toml` (such as `pants.backend.build_files.fmt.black` [or others](https://www.pantsbuild.org/v2.13/docs/enabling-backends)). Then to format, run `./pants fmt '**/BUILD'` or `./pants fmt ::` (formats everything). +You can autoformat `BUILD` files by enabling a `BUILD` file formatter by adding it to `[GLOBAL].backend_packages` in `pants.toml` (such as `pants.backend.build_files.fmt.black` [or others](doc:enabling-backends)). Then to format, run `./pants fmt '**/BUILD'` or `./pants fmt ::` (formats everything). Target addresses ================ From 07613b8c010d6beb12120c0dcacb659540c143a4 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Mon, 22 Aug 2022 12:16:48 -0500 Subject: [PATCH 14/15] ann # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- src/python/pants/backend/python/lint/black/rules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/python/pants/backend/python/lint/black/rules.py b/src/python/pants/backend/python/lint/black/rules.py index 4c5d5f25d91..72e6f36e6e0 100644 --- a/src/python/pants/backend/python/lint/black/rules.py +++ b/src/python/pants/backend/python/lint/black/rules.py @@ -1,6 +1,8 @@ # Copyright 2019 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). +from __future__ import annotations + from pants.backend.python.lint.black.subsystem import Black, BlackFieldSet from pants.backend.python.subsystems.setup import PythonSetup from pants.backend.python.util_rules import pex From 6fdcabe520eb3c362ea27d04cb80334838946740 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Mon, 22 Aug 2022 12:22:26 -0500 Subject: [PATCH 15/15] docs # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- docs/markdown/Using Pants/concepts/enabling-backends.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/markdown/Using Pants/concepts/enabling-backends.md b/docs/markdown/Using Pants/concepts/enabling-backends.md index 3b124cf166f..b10967348e9 100644 --- a/docs/markdown/Using Pants/concepts/enabling-backends.md +++ b/docs/markdown/Using Pants/concepts/enabling-backends.md @@ -22,6 +22,8 @@ Available backends | Backend | What it does | Docs | | :-------------------------------------------------------- | :------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------- | +| `pants.backend.build_files.fmt.black` | Enables autoformatting `BUILD` files using `black`. | | +| `pants.backend.build_files.fmt.yapf` | Enables autoformatting `BUILD` files using `yapf`. | | | `pants.backend.awslambda.python` | Enables generating an AWS Lambda zip file from Python code. | [AWS Lambda](doc:awslambda-python) | | `pants.backend.codegen.protobuf.lint.buf` | Activate the Buf formatter and linter for Protocol Buffers. | [Protobuf](doc:protobuf-python) | | `pants.backend.codegen.protobuf.python` | Enables generating Python from Protocol Buffers. Includes gRPC support. | [Protobuf and gRPC](doc:protobuf-python) | @@ -38,7 +40,7 @@ Available backends | `pants.backend.experimental.python.lint.pyupgrade` | Enables Pyupgrade, which upgrades to new Python syntax: | [Linters and formatters](doc:python-linters-and-formatters) | | `pants.backend.experimental.python.packaging.pyoxidizer` | Enables `pyoxidizer_binary` target. | [PyOxidizer](doc:pyoxidizer) | | `pants.backend.google_cloud_function.python` | Enables generating a Google Cloud Function from Python code. | [Google Cloud Function](doc:google-cloud-function-python) | -| `pants.backend.plugin_development` | Enables `pants_requirements` target. | [Plugins overview](doc:plugins-overview) | +| `pants.backend.plugin_development` | Enables `pants_requirements` target. | [Plugins overview](doc:plugins-overview) | | `pants.backend.python` | Core Python support. | [Enabling Python support](doc:python-backend) | | `pants.backend.python.mixed_interpreter_constraints` | Adds the `py-constraints` goal for insights on Python interpreter constraints. | [Interpreter compatibility](doc:python-interpreter-compatibility) | | `pants.backend.python.lint.bandit` | Enables Bandit, the Python security linter: . | [Linters and formatters](doc:python-linters-and-formatters) |