diff --git a/src/python/pants/backend/codegen/protobuf/python/python_protobuf_subsystem.py b/src/python/pants/backend/codegen/protobuf/python/python_protobuf_subsystem.py index 66331af9230..36b24c5d25d 100644 --- a/src/python/pants/backend/codegen/protobuf/python/python_protobuf_subsystem.py +++ b/src/python/pants/backend/codegen/protobuf/python/python_protobuf_subsystem.py @@ -4,8 +4,10 @@ from typing import cast from pants.backend.codegen.protobuf.target_types import ProtobufDependenciesField -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.subsystems.python_tool_base import PythonToolRequirementsBase +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.engine.addresses import Addresses, UnparsedAddressInputs from pants.engine.rules import Get, collect_rules, rule from pants.engine.target import InjectDependenciesRequest, InjectedDependencies @@ -72,7 +74,7 @@ class PythonProtobufMypyPlugin(PythonToolRequirementsBase): default_lockfile_url = git_url(default_lockfile_path) -class MypyProtobufLockfileSentinel(PythonToolLockfileSentinel): +class MypyProtobufLockfileSentinel(ToolLockfileSentinel): options_scope = PythonProtobufMypyPlugin.options_scope @@ -98,6 +100,7 @@ async def inject_dependencies( def rules(): return [ *collect_rules(), + *lockfile.rules(), UnionRule(InjectDependenciesRequest, InjectPythonProtobufDependencies), - UnionRule(PythonToolLockfileSentinel, MypyProtobufLockfileSentinel), + UnionRule(ToolLockfileSentinel, MypyProtobufLockfileSentinel), ] diff --git a/src/python/pants/backend/docker/subsystems/dockerfile_parser.py b/src/python/pants/backend/docker/subsystems/dockerfile_parser.py index 3ed12ac5594..f6f39bd642b 100644 --- a/src/python/pants/backend/docker/subsystems/dockerfile_parser.py +++ b/src/python/pants/backend/docker/subsystems/dockerfile_parser.py @@ -10,10 +10,12 @@ from pants.backend.docker.target_types import DockerImageSourceField from pants.backend.docker.util_rules.docker_build_args import DockerBuildArgs -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.subsystems.python_tool_base import PythonToolRequirementsBase from pants.backend.python.target_types import EntryPoint from pants.backend.python.util_rules.pex import PexRequest, VenvPex, VenvPexProcess +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.engine.addresses import Address from pants.engine.fs import CreateDigest, Digest, FileContent from pants.engine.process import Process, ProcessResult @@ -44,7 +46,7 @@ class DockerfileParser(PythonToolRequirementsBase): default_lockfile_url = git_url(default_lockfile_path) -class DockerfileParserLockfileSentinel(PythonToolLockfileSentinel): +class DockerfileParserLockfileSentinel(ToolLockfileSentinel): options_scope = DockerfileParser.options_scope @@ -198,5 +200,6 @@ async def parse_dockerfile(request: DockerfileInfoRequest) -> DockerfileInfo: def rules(): return ( *collect_rules(), - UnionRule(PythonToolLockfileSentinel, DockerfileParserLockfileSentinel), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, DockerfileParserLockfileSentinel), ) diff --git a/src/python/pants/backend/experimental/python/user_lockfiles.py b/src/python/pants/backend/experimental/python/user_lockfiles.py index c921a3d2fe4..a95b69251c7 100644 --- a/src/python/pants/backend/experimental/python/user_lockfiles.py +++ b/src/python/pants/backend/experimental/python/user_lockfiles.py @@ -5,11 +5,12 @@ import logging -from pants.backend.python.goals.lockfile import PythonLockfile, PythonLockfileRequest +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.subsystems.setup import PythonSetup from pants.backend.python.target_types import PythonRequirementsField from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints from pants.backend.python.util_rules.pex import PexRequirements +from pants.core.goals.generate_lockfiles import Lockfile from pants.engine.addresses import Addresses from pants.engine.fs import Workspace from pants.engine.goal import Goal, GoalSubsystem @@ -65,11 +66,11 @@ async def generate_user_lockfile_goal( return GenerateUserLockfileGoal(exit_code=0) result = await Get( - PythonLockfile, + Lockfile, PythonLockfileRequest( - req_strings, + requirements=req_strings, # TODO(#12314): Use interpreter constraints from the transitive closure. - InterpreterConstraints(python_setup.interpreter_constraints), + interpreter_constraints=InterpreterConstraints(python_setup.interpreter_constraints), resolve_name="not yet implemented", lockfile_dest=python_setup.lockfile, _description=( diff --git a/src/python/pants/backend/python/goals/coverage_py.py b/src/python/pants/backend/python/goals/coverage_py.py index db4c1f53be9..481e16ec63b 100644 --- a/src/python/pants/backend/python/goals/coverage_py.py +++ b/src/python/pants/backend/python/goals/coverage_py.py @@ -12,7 +12,8 @@ import toml -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.target_types import ConsoleScript from pants.backend.python.util_rules.pex import PexRequest, VenvPex, VenvPexProcess @@ -20,6 +21,7 @@ PythonSourceFiles, PythonSourceFilesRequest, ) +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.core.goals.test import ( ConsoleCoverageReport, CoverageData, @@ -234,7 +236,7 @@ def fail_under(self) -> int: return cast(int, self.options.fail_under) -class CoveragePyLockfileSentinel(PythonToolLockfileSentinel): +class CoveragePyLockfileSentinel(ToolLockfileSentinel): options_scope = CoverageSubsystem.options_scope @@ -606,6 +608,7 @@ def _get_coverage_report( def rules(): return [ *collect_rules(), + *lockfile.rules(), UnionRule(CoverageDataCollection, PytestCoverageDataCollection), - UnionRule(PythonToolLockfileSentinel, CoveragePyLockfileSentinel), + UnionRule(ToolLockfileSentinel, CoveragePyLockfileSentinel), ] diff --git a/src/python/pants/backend/python/goals/lockfile.py b/src/python/pants/backend/python/goals/lockfile.py index 9f2b190a678..112ce58adec 100644 --- a/src/python/pants/backend/python/goals/lockfile.py +++ b/src/python/pants/backend/python/goals/lockfile.py @@ -7,7 +7,7 @@ from collections import defaultdict from dataclasses import dataclass from pathlib import PurePath -from typing import ClassVar, Iterable, Sequence, cast +from typing import Iterable from pants.backend.python.pip_requirement import PipRequirement from pants.backend.python.subsystems.poetry import ( @@ -15,18 +15,13 @@ PoetrySubsystem, create_pyproject_toml, ) -from pants.backend.python.subsystems.python_tool_base import ( - DEFAULT_TOOL_LOCKFILE, - NO_TOOL_LOCKFILE, - PythonToolRequirementsBase, -) +from pants.backend.python.subsystems.python_tool_base import PythonToolRequirementsBase from pants.backend.python.subsystems.repos import PythonRepos from pants.backend.python.subsystems.setup import PythonSetup from pants.backend.python.target_types import ( EntryPoint, PythonRequirementCompatibleResolvesField, PythonRequirementsField, - UnrecognizedResolveNamesError, ) from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints from pants.backend.python.util_rules.lockfile_metadata import ( @@ -34,97 +29,31 @@ calculate_invalidation_digest, ) from pants.backend.python.util_rules.pex import PexRequest, PexRequirements, VenvPex, VenvPexProcess -from pants.engine.collection import Collection -from pants.engine.fs import ( - CreateDigest, - Digest, - DigestContents, - FileContent, - MergeDigests, - Workspace, +from pants.core.goals.generate_lockfiles import ( + GenerateLockfilesSubsystem, + KnownUserResolveNames, + KnownUserResolveNamesRequest, + Lockfile, + LockfileRequest, + RequestedUserResolveNames, + UserLockfileRequests, + WrappedLockfileRequest, ) -from pants.engine.goal import Goal, GoalSubsystem +from pants.engine.fs import CreateDigest, Digest, DigestContents, FileContent from pants.engine.process import ProcessCacheScope, ProcessResult -from pants.engine.rules import Get, MultiGet, collect_rules, goal_rule, rule +from pants.engine.rules import Get, MultiGet, collect_rules, rule from pants.engine.target import AllTargets -from pants.engine.unions import UnionMembership, union +from pants.engine.unions import UnionRule from pants.util.logging import LogLevel from pants.util.ordered_set import FrozenOrderedSet logger = logging.getLogger(__name__) -@union -class PythonToolLockfileSentinel: - options_scope: ClassVar[str] - - -class GenerateLockfilesSubsystem(GoalSubsystem): - name = "generate-lockfiles" - help = "Generate lockfiles for Python third-party dependencies." - required_union_implementations = (PythonToolLockfileSentinel,) - - @classmethod - def register_options(cls, register) -> None: - super().register_options(register) - register( - "--resolve", - type=list, - member_type=str, - advanced=False, - help=( - "Only generate lockfiles for the specified resolve(s).\n\n" - "Resolves are the logical names for the different lockfiles used in your project. " - "For your own code's dependencies, these come from the option " - "`[python].experimental_resolves`. For tool lockfiles, resolve " - "names are the options scope for that tool such as `black`, `pytest`, and " - "`mypy-protobuf`.\n\n" - "For example, you can run `./pants generate-lockfiles --resolve=black " - "--resolve=pytest --resolve=data-science` to only generate lockfiles for those " - "two tools and your resolve named `data-science`.\n\n" - "If you specify an invalid resolve name, like 'fake', Pants will output all " - "possible values.\n\n" - "If not specified, Pants will generate lockfiles for all resolves." - ), - ) - register( - "--custom-command", - advanced=True, - type=str, - default=None, - help=( - "If set, lockfile headers will say to run this command to regenerate the lockfile, " - "rather than running `./pants generate-lockfiles --resolve=` like normal." - ), - ) - - @property - def resolve_names(self) -> tuple[str, ...]: - return tuple(self.options.resolve) - - @property - def custom_command(self) -> str | None: - return cast("str | None", self.options.custom_command) - - -# -------------------------------------------------------------------------------------- -# Generic lockfile generation -# -------------------------------------------------------------------------------------- - - -@dataclass(frozen=True) -class PythonLockfile: - digest: Digest - resolve_name: str - path: str - - @dataclass(frozen=True) -class PythonLockfileRequest: +class PythonLockfileRequest(LockfileRequest): requirements: FrozenOrderedSet[str] interpreter_constraints: InterpreterConstraints - resolve_name: str - lockfile_dest: str # Only kept for `[python].experimental_lockfile`, which is not using the new # "named resolve" semantics yet. _description: str | None = None @@ -146,8 +75,8 @@ def from_tool( """ if not subsystem.uses_lockfile: return cls( - FrozenOrderedSet(), - InterpreterConstraints(), + requirements=FrozenOrderedSet(), + interpreter_constraints=InterpreterConstraints(), resolve_name=subsystem.options_scope, lockfile_dest=subsystem.lockfile, ) @@ -168,12 +97,43 @@ def requirements_hex_digest(self) -> str: return calculate_invalidation_digest(self.requirements) -@rule(desc="Generate lockfile", level=LogLevel.DEBUG) +@rule +def wrap_python_lockfile_request(request: PythonLockfileRequest) -> WrappedLockfileRequest: + return WrappedLockfileRequest(request) + + +class MaybeWarnPythonRepos: + pass + + +@rule +def maybe_warn_python_repos(python_repos: PythonRepos) -> MaybeWarnPythonRepos: + def warn_python_repos(option: str) -> None: + logger.warning( + f"The option `[python-repos].{option}` is configured, but it does not currently work " + "with lockfile generation. Lockfile generation will fail if the relevant requirements " + "cannot be located on PyPI.\n\n" + "If lockfile generation fails, you can disable lockfiles by setting " + "`[tool].lockfile = ''`, e.g. setting `[black].lockfile`. You can also manually " + "generate a lockfile, such as by using pip-compile or `pip freeze`. Set the " + "`[tool].lockfile` option to the path you manually generated. When manually maintaining " + "lockfiles, set `[python].invalid_lockfile_behavior = 'ignore'." + ) + + if python_repos.repos: + warn_python_repos("repos") + if python_repos.indexes != [python_repos.pypi_index]: + warn_python_repos("indexes") + return MaybeWarnPythonRepos() + + +@rule(desc="Generate Python lockfile", level=LogLevel.DEBUG) async def generate_lockfile( req: PythonLockfileRequest, poetry_subsystem: PoetrySubsystem, generate_lockfiles_subsystem: GenerateLockfilesSubsystem, -) -> PythonLockfile: + _: MaybeWarnPythonRepos, +) -> Lockfile: pyproject_toml = create_pyproject_toml(req.requirements, req.interpreter_constraints).encode() pyproject_toml_digest, launcher_digest = await MultiGet( Get(Digest, CreateDigest([FileContent("pyproject.toml", pyproject_toml)])), @@ -248,28 +208,34 @@ async def generate_lockfile( final_lockfile_digest = await Get( Digest, CreateDigest([FileContent(req.lockfile_dest, lockfile_with_header)]) ) - return PythonLockfile(final_lockfile_digest, req.resolve_name, req.lockfile_dest) + return Lockfile(final_lockfile_digest, req.resolve_name, req.lockfile_dest) -# -------------------------------------------------------------------------------------- -# User lockfiles -# -------------------------------------------------------------------------------------- +class RequestedPythonUserResolveNames(RequestedUserResolveNames): + pass -class _SpecifiedUserResolves(Collection[str]): +class KnownPythonUserResolveNamesRequest(KnownUserResolveNamesRequest): pass -class _UserLockfileRequests(Collection[PythonLockfileRequest]): - pass +@rule +def determine_python_user_resolves( + _: KnownPythonUserResolveNamesRequest, python_setup: PythonSetup +) -> KnownUserResolveNames: + return KnownUserResolveNames( + names=tuple(python_setup.resolves.keys()), + option_name="[python].experimental_resolves", + requested_resolve_names_cls=RequestedPythonUserResolveNames, + ) @rule async def setup_user_lockfile_requests( - requested: _SpecifiedUserResolves, all_targets: AllTargets, python_setup: PythonSetup -) -> _UserLockfileRequests: + requested: RequestedPythonUserResolveNames, all_targets: AllTargets, python_setup: PythonSetup +) -> UserLockfileRequests: if not python_setup.enable_resolves: - return _UserLockfileRequests() + return UserLockfileRequests() resolve_to_requirements_fields = defaultdict(set) for tgt in all_targets: @@ -284,13 +250,13 @@ async def setup_user_lockfile_requests( # inspect all consumers of that resolve or start to closely couple the resolve with the # interpreter constraints (a "context"). - return _UserLockfileRequests( + return UserLockfileRequests( PythonLockfileRequest( - PexRequirements.create_from_requirement_fields( + requirements=PexRequirements.create_from_requirement_fields( resolve_to_requirements_fields[resolve], constraints_strings=(), ).req_strings, - InterpreterConstraints(python_setup.interpreter_constraints), + interpreter_constraints=InterpreterConstraints(python_setup.interpreter_constraints), resolve_name=resolve, lockfile_dest=python_setup.resolves[resolve], ) @@ -298,158 +264,10 @@ async def setup_user_lockfile_requests( ) -# -------------------------------------------------------------------------------------- -# Lock goal -# -------------------------------------------------------------------------------------- - - -class GenerateLockfilesGoal(Goal): - subsystem_cls = GenerateLockfilesSubsystem - - -@goal_rule -async def generate_lockfiles_goal( - workspace: Workspace, - union_membership: UnionMembership, - generate_lockfiles_subsystem: GenerateLockfilesSubsystem, - python_setup: PythonSetup, - python_repos: PythonRepos, -) -> GenerateLockfilesGoal: - if python_repos.repos: - warn_python_repos("repos") - if python_repos.indexes != [python_repos.pypi_index]: - warn_python_repos("indexes") - - specified_user_resolves, specified_tool_sentinels = determine_resolves_to_generate( - python_setup.resolves.keys(), - union_membership[PythonToolLockfileSentinel], - generate_lockfiles_subsystem.resolve_names, - ) - - specified_user_requests = await Get( - _UserLockfileRequests, _SpecifiedUserResolves(specified_user_resolves) - ) - specified_tool_requests = await MultiGet( - Get(PythonLockfileRequest, PythonToolLockfileSentinel, sentinel()) - for sentinel in specified_tool_sentinels - ) - applicable_tool_requests = filter_tool_lockfile_requests( - specified_tool_requests, - resolve_specified=bool(generate_lockfiles_subsystem.resolve_names), - ) - - results = await MultiGet( - Get(PythonLockfile, PythonLockfileRequest, req) - for req in (*specified_user_requests, *applicable_tool_requests) - ) - - merged_digest = await Get(Digest, MergeDigests(res.digest for res in results)) - workspace.write_digest(merged_digest) - for result in results: - logger.info(f"Wrote lockfile for the resolve `{result.resolve_name}` to {result.path}") - - return GenerateLockfilesGoal(exit_code=0) - - -def warn_python_repos(option: str) -> None: - logger.warning( - f"The option `[python-repos].{option}` is configured, but it does not currently work " - "with lockfile generation. Lockfile generation will fail if the relevant requirements " - "cannot be located on PyPI.\n\n" - "If lockfile generation fails, you can disable lockfiles by setting " - "`[tool].lockfile = ''`, e.g. setting `[black].lockfile`. You can also manually " - "generate a lockfile, such as by using pip-compile or `pip freeze`. Set the " - "`[tool].lockfile` option to the path you manually generated. When manually maintaining " - "lockfiles, set `[python].invalid_lockfile_behavior = 'ignore'." - ) - - -class AmbiguousResolveNamesError(Exception): - def __init__(self, ambiguous_names: list[str]) -> None: - if len(ambiguous_names) == 1: - first_paragraph = ( - "A resolve name from the option `[python].experimental_resolves` collides with the " - f"name of a tool resolve: {ambiguous_names[0]}" - ) - else: - first_paragraph = ( - "Some resolve names from the option `[python].experimental_resolves` collide with " - f"the names of tool resolves: {sorted(ambiguous_names)}" - ) - super().__init__( - f"{first_paragraph}\n\n" - "To fix, please update `[python].experimental_resolves` to use different resolve names." - ) - - -def determine_resolves_to_generate( - all_user_resolves: Iterable[str], - all_tool_sentinels: Iterable[type[PythonToolLockfileSentinel]], - requested_resolve_names: Sequence[str], -) -> tuple[list[str], list[type[PythonToolLockfileSentinel]]]: - """Apply the `--resolve` option to determine which resolves are specified. - - Return a tuple of `(user_resolves, tool_lockfile_sentinels)`. - """ - resolve_names_to_sentinels = { - sentinel.options_scope: sentinel for sentinel in all_tool_sentinels - } - - ambiguous_resolve_names = [ - resolve_name - for resolve_name in all_user_resolves - if resolve_name in resolve_names_to_sentinels - ] - if ambiguous_resolve_names: - raise AmbiguousResolveNamesError(ambiguous_resolve_names) - - if not requested_resolve_names: - return list(all_user_resolves), list(all_tool_sentinels) - - specified_user_resolves = [] - specified_sentinels = [] - unrecognized_resolve_names = [] - for resolve_name in requested_resolve_names: - sentinel = resolve_names_to_sentinels.get(resolve_name) - if sentinel: - specified_sentinels.append(sentinel) - elif resolve_name in all_user_resolves: - specified_user_resolves.append(resolve_name) - else: - unrecognized_resolve_names.append(resolve_name) - - if unrecognized_resolve_names: - raise UnrecognizedResolveNamesError( - unrecognized_resolve_names, - {*all_user_resolves, *resolve_names_to_sentinels.keys()}, - description_of_origin="the option `--generate-lockfiles-resolve`", - ) - - return specified_user_resolves, specified_sentinels - - -def filter_tool_lockfile_requests( - specified_requests: Sequence[PythonLockfileRequest], *, resolve_specified: bool -) -> list[PythonLockfileRequest]: - result = [] - for req in specified_requests: - if req.lockfile_dest not in (NO_TOOL_LOCKFILE, DEFAULT_TOOL_LOCKFILE): - result.append(req) - continue - if resolve_specified: - resolve = req.resolve_name - raise ValueError( - f"You requested to generate a lockfile for {resolve} because " - "you included it in `--generate-lockfiles-resolve`, but " - f"`[{resolve}].lockfile` is set to `{req.lockfile_dest}` " - "so a lockfile will not be generated.\n\n" - f"If you would like to generate a lockfile for {resolve}, please " - f"set `[{resolve}].lockfile` to the path where it should be " - "generated and run again." - ) - - return result - - def rules(): - return collect_rules() + return ( + *collect_rules(), + UnionRule(LockfileRequest, PythonLockfileRequest), + UnionRule(KnownUserResolveNamesRequest, KnownPythonUserResolveNamesRequest), + UnionRule(RequestedUserResolveNames, RequestedPythonUserResolveNames), + ) diff --git a/src/python/pants/backend/python/goals/lockfile_test.py b/src/python/pants/backend/python/goals/lockfile_test.py index 2b860c5ca49..f94dee001f6 100644 --- a/src/python/pants/backend/python/goals/lockfile_test.py +++ b/src/python/pants/backend/python/goals/lockfile_test.py @@ -5,128 +5,26 @@ from textwrap import dedent -import pytest - from pants.backend.python.goals.lockfile import ( - AmbiguousResolveNamesError, PythonLockfileRequest, - PythonToolLockfileSentinel, - _SpecifiedUserResolves, - _UserLockfileRequests, - determine_resolves_to_generate, - filter_tool_lockfile_requests, + RequestedPythonUserResolveNames, setup_user_lockfile_requests, ) -from pants.backend.python.subsystems.python_tool_base import DEFAULT_TOOL_LOCKFILE, NO_TOOL_LOCKFILE from pants.backend.python.subsystems.setup import PythonSetup -from pants.backend.python.target_types import PythonRequirementTarget, UnrecognizedResolveNamesError +from pants.backend.python.target_types import PythonRequirementTarget from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints +from pants.core.goals.generate_lockfiles import UserLockfileRequests from pants.engine.rules import SubsystemRule from pants.testutil.rule_runner import PYTHON_BOOTSTRAP_ENV, QueryRule, RuleRunner from pants.util.ordered_set import FrozenOrderedSet -def test_determine_tool_sentinels_to_generate() -> None: - class Tool1(PythonToolLockfileSentinel): - options_scope = "tool1" - - class Tool2(PythonToolLockfileSentinel): - options_scope = "tool2" - - class Tool3(PythonToolLockfileSentinel): - options_scope = "tool3" - - all_user_resolves = ["u1", "u2", "u3"] - - def assert_chosen( - requested: list[str], - expected_user_resolves: list[str], - expected_tools: list[type[PythonToolLockfileSentinel]], - ) -> None: - user_resolves, tools = determine_resolves_to_generate( - all_user_resolves, [Tool1, Tool2, Tool3], requested - ) - assert user_resolves == expected_user_resolves - assert tools == expected_tools - - assert_chosen( - [Tool2.options_scope, "u2"], expected_user_resolves=["u2"], expected_tools=[Tool2] - ) - assert_chosen( - [Tool1.options_scope, Tool3.options_scope], - expected_user_resolves=[], - expected_tools=[Tool1, Tool3], - ) - - # If none are specifically requested, return all. - assert_chosen( - [], expected_user_resolves=["u1", "u2", "u3"], expected_tools=[Tool1, Tool2, Tool3] - ) - - with pytest.raises(UnrecognizedResolveNamesError): - assert_chosen(["fake"], expected_user_resolves=[], expected_tools=[]) - - # Error if same resolve name used for tool lockfiles and user lockfiles. - class AmbiguousTool(PythonToolLockfileSentinel): - options_scope = "ambiguous" - - with pytest.raises(AmbiguousResolveNamesError): - determine_resolves_to_generate( - {"ambiguous": "lockfile.txt"}, [AmbiguousTool], ["ambiguous"] - ) - - -def test_filter_tool_lockfile_requests() -> None: - def create_request(name: str, lockfile_dest: str | None = None) -> PythonLockfileRequest: - return PythonLockfileRequest( - FrozenOrderedSet(), - InterpreterConstraints(), - resolve_name=name, - lockfile_dest=lockfile_dest or f"{name}.txt", - ) - - tool1 = create_request("tool1") - tool2 = create_request("tool2") - disabled_tool = create_request("none", lockfile_dest=NO_TOOL_LOCKFILE) - default_tool = create_request("default", lockfile_dest=DEFAULT_TOOL_LOCKFILE) - - def assert_filtered( - extra_request: PythonLockfileRequest | None, - *, - resolve_specified: bool, - ) -> None: - requests = [tool1, tool2] - if extra_request: - requests.append(extra_request) - assert filter_tool_lockfile_requests(requests, resolve_specified=resolve_specified) == [ - tool1, - tool2, - ] - - assert_filtered(None, resolve_specified=False) - assert_filtered(None, resolve_specified=True) - - assert_filtered(disabled_tool, resolve_specified=False) - with pytest.raises(ValueError) as exc: - assert_filtered(disabled_tool, resolve_specified=True) - assert f"`[{disabled_tool.resolve_name}].lockfile` is set to `{NO_TOOL_LOCKFILE}`" in str( - exc.value - ) - - assert_filtered(default_tool, resolve_specified=False) - with pytest.raises(ValueError) as exc: - assert_filtered(default_tool, resolve_specified=True) - assert f"`[{default_tool.resolve_name}].lockfile` is set to `{DEFAULT_TOOL_LOCKFILE}`" in str( - exc.value - ) - - def test_multiple_resolves() -> None: rule_runner = RuleRunner( rules=[ setup_user_lockfile_requests, SubsystemRule(PythonSetup), - QueryRule(_UserLockfileRequests, [_SpecifiedUserResolves]), + QueryRule(UserLockfileRequests, [RequestedPythonUserResolveNames]), ], target_types=[PythonRequirementTarget], ) @@ -160,17 +58,23 @@ def test_multiple_resolves() -> None: ], env_inherit=PYTHON_BOOTSTRAP_ENV, ) - result = rule_runner.request(_UserLockfileRequests, [_SpecifiedUserResolves(["a", "b"])]) + result = rule_runner.request( + UserLockfileRequests, [RequestedPythonUserResolveNames(["a", "b"])] + ) assert set(result) == { PythonLockfileRequest( - FrozenOrderedSet(["a", "both1", "both2"]), - InterpreterConstraints(PythonSetup.default_interpreter_constraints), + requirements=FrozenOrderedSet(["a", "both1", "both2"]), + interpreter_constraints=InterpreterConstraints( + PythonSetup.default_interpreter_constraints + ), resolve_name="a", lockfile_dest="a.lock", ), PythonLockfileRequest( - FrozenOrderedSet(["b", "both1", "both2"]), - InterpreterConstraints(PythonSetup.default_interpreter_constraints), + requirements=FrozenOrderedSet(["b", "both1", "both2"]), + interpreter_constraints=InterpreterConstraints( + PythonSetup.default_interpreter_constraints + ), resolve_name="b", lockfile_dest="b.lock", ), diff --git a/src/python/pants/backend/python/lint/autoflake/subsystem.py b/src/python/pants/backend/python/lint/autoflake/subsystem.py index 1dd7aa1a1df..752dc452fc8 100644 --- a/src/python/pants/backend/python/lint/autoflake/subsystem.py +++ b/src/python/pants/backend/python/lint/autoflake/subsystem.py @@ -5,9 +5,11 @@ from typing import cast -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.target_types import ConsoleScript +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.engine.rules import collect_rules, rule from pants.engine.unions import UnionRule from pants.option.custom_types import shell_str @@ -60,7 +62,7 @@ def args(self) -> tuple[str, ...]: return tuple(self.options.args) -class AutoflakeLockfileSentinel(PythonToolLockfileSentinel): +class AutoflakeLockfileSentinel(ToolLockfileSentinel): options_scope = Autoflake.options_scope @@ -72,4 +74,8 @@ async def setup_autoflake_lockfile( def rules(): - return (*collect_rules(), UnionRule(PythonToolLockfileSentinel, AutoflakeLockfileSentinel)) + return ( + *collect_rules(), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, AutoflakeLockfileSentinel), + ) diff --git a/src/python/pants/backend/python/lint/bandit/subsystem.py b/src/python/pants/backend/python/lint/bandit/subsystem.py index 0c2f69cb462..2934fdc78ee 100644 --- a/src/python/pants/backend/python/lint/bandit/subsystem.py +++ b/src/python/pants/backend/python/lint/bandit/subsystem.py @@ -7,7 +7,8 @@ from dataclasses import dataclass from typing import cast -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.lint.bandit.skip_field import SkipBanditField from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.subsystems.setup import PythonSetup @@ -17,6 +18,7 @@ PythonSourceField, ) from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.core.util_rules.config_files import ConfigFilesRequest from pants.engine.rules import Get, collect_rules, rule from pants.engine.target import AllTargets, AllTargetsRequest, FieldSet, Target @@ -109,7 +111,7 @@ def config_request(self) -> ConfigFilesRequest: ) -class BanditLockfileSentinel(PythonToolLockfileSentinel): +class BanditLockfileSentinel(ToolLockfileSentinel): options_scope = Bandit.options_scope @@ -144,4 +146,8 @@ async def setup_bandit_lockfile( def rules(): - return (*collect_rules(), UnionRule(PythonToolLockfileSentinel, BanditLockfileSentinel)) + return ( + *collect_rules(), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, BanditLockfileSentinel), + ) diff --git a/src/python/pants/backend/python/lint/black/subsystem.py b/src/python/pants/backend/python/lint/black/subsystem.py index 72cad3a3663..26d60bb0e85 100644 --- a/src/python/pants/backend/python/lint/black/subsystem.py +++ b/src/python/pants/backend/python/lint/black/subsystem.py @@ -6,12 +6,14 @@ import os.path from typing import Iterable, cast -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.lint.black.skip_field import SkipBlackField from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.subsystems.setup import PythonSetup from pants.backend.python.target_types import ConsoleScript from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.core.util_rules.config_files import ConfigFilesRequest from pants.engine.rules import Get, collect_rules, rule from pants.engine.target import AllTargets, AllTargetsRequest @@ -105,7 +107,7 @@ def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest: ) -class BlackLockfileSentinel(PythonToolLockfileSentinel): +class BlackLockfileSentinel(ToolLockfileSentinel): options_scope = Black.options_scope @@ -133,4 +135,8 @@ async def setup_black_lockfile( def rules(): - return (*collect_rules(), UnionRule(PythonToolLockfileSentinel, BlackLockfileSentinel)) + return ( + *collect_rules(), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, BlackLockfileSentinel), + ) diff --git a/src/python/pants/backend/python/lint/docformatter/subsystem.py b/src/python/pants/backend/python/lint/docformatter/subsystem.py index 41fedf38283..bd84c2445e5 100644 --- a/src/python/pants/backend/python/lint/docformatter/subsystem.py +++ b/src/python/pants/backend/python/lint/docformatter/subsystem.py @@ -3,9 +3,11 @@ from typing import Tuple, cast -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.target_types import ConsoleScript +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.engine.rules import collect_rules, rule from pants.engine.unions import UnionRule from pants.option.custom_types import shell_str @@ -58,7 +60,7 @@ def args(self) -> Tuple[str, ...]: return tuple(self.options.args) -class DocformatterLockfileSentinel(PythonToolLockfileSentinel): +class DocformatterLockfileSentinel(ToolLockfileSentinel): options_scope = Docformatter.options_scope @@ -70,4 +72,8 @@ def setup_lockfile_request( def rules(): - return (*collect_rules(), UnionRule(PythonToolLockfileSentinel, DocformatterLockfileSentinel)) + return ( + *collect_rules(), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, DocformatterLockfileSentinel), + ) diff --git a/src/python/pants/backend/python/lint/flake8/subsystem.py b/src/python/pants/backend/python/lint/flake8/subsystem.py index c9a66ab8d9d..5a732c590fd 100644 --- a/src/python/pants/backend/python/lint/flake8/subsystem.py +++ b/src/python/pants/backend/python/lint/flake8/subsystem.py @@ -7,7 +7,8 @@ from dataclasses import dataclass from typing import cast -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.lint.flake8.skip_field import SkipFlake8Field from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.subsystems.setup import PythonSetup @@ -17,6 +18,7 @@ PythonSourceField, ) from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.core.util_rules.config_files import ConfigFilesRequest from pants.engine.rules import Get, collect_rules, rule from pants.engine.target import AllTargets, AllTargetsRequest, FieldSet, Target @@ -118,7 +120,7 @@ def config_request(self) -> ConfigFilesRequest: ) -class Flake8LockfileSentinel(PythonToolLockfileSentinel): +class Flake8LockfileSentinel(ToolLockfileSentinel): options_scope = Flake8.options_scope @@ -153,4 +155,8 @@ async def setup_flake8_lockfile( def rules(): - return (*collect_rules(), UnionRule(PythonToolLockfileSentinel, Flake8LockfileSentinel)) + return ( + *collect_rules(), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, Flake8LockfileSentinel), + ) diff --git a/src/python/pants/backend/python/lint/isort/subsystem.py b/src/python/pants/backend/python/lint/isort/subsystem.py index 6df0f6023e0..e38a221a3ba 100644 --- a/src/python/pants/backend/python/lint/isort/subsystem.py +++ b/src/python/pants/backend/python/lint/isort/subsystem.py @@ -6,9 +6,11 @@ import os.path from typing import Iterable, cast -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.target_types import ConsoleScript +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.core.util_rules.config_files import ConfigFilesRequest from pants.engine.rules import collect_rules, rule from pants.engine.unions import UnionRule @@ -124,7 +126,7 @@ def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest: ) -class IsortLockfileSentinel(PythonToolLockfileSentinel): +class IsortLockfileSentinel(ToolLockfileSentinel): options_scope = Isort.options_scope @@ -134,4 +136,8 @@ def setup_isort_lockfile(_: IsortLockfileSentinel, isort: Isort) -> PythonLockfi def rules(): - return (*collect_rules(), UnionRule(PythonToolLockfileSentinel, IsortLockfileSentinel)) + return ( + *collect_rules(), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, IsortLockfileSentinel), + ) diff --git a/src/python/pants/backend/python/lint/pylint/subsystem.py b/src/python/pants/backend/python/lint/pylint/subsystem.py index 425d39fa5ab..cef68deded3 100644 --- a/src/python/pants/backend/python/lint/pylint/subsystem.py +++ b/src/python/pants/backend/python/lint/pylint/subsystem.py @@ -8,7 +8,8 @@ from dataclasses import dataclass from typing import Iterable, cast -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.lint.pylint.skip_field import SkipPylintField from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.subsystems.setup import PythonSetup @@ -24,6 +25,7 @@ PythonSourceFilesRequest, StrippedPythonSourceFiles, ) +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.core.util_rules.config_files import ConfigFilesRequest from pants.engine.addresses import Addresses, UnparsedAddressInputs from pants.engine.fs import EMPTY_DIGEST, AddPrefix, Digest @@ -232,7 +234,7 @@ async def pylint_first_party_plugins(pylint: Pylint) -> PylintFirstPartyPlugins: # -------------------------------------------------------------------------------------- -class PylintLockfileSentinel(PythonToolLockfileSentinel): +class PylintLockfileSentinel(ToolLockfileSentinel): options_scope = Pylint.options_scope @@ -295,4 +297,8 @@ async def setup_pylint_lockfile( def rules(): - return (*collect_rules(), UnionRule(PythonToolLockfileSentinel, PylintLockfileSentinel)) + return ( + *collect_rules(), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, PylintLockfileSentinel), + ) diff --git a/src/python/pants/backend/python/lint/pyupgrade/subsystem.py b/src/python/pants/backend/python/lint/pyupgrade/subsystem.py index 43d3ff5cd85..76da5291acf 100644 --- a/src/python/pants/backend/python/lint/pyupgrade/subsystem.py +++ b/src/python/pants/backend/python/lint/pyupgrade/subsystem.py @@ -5,9 +5,11 @@ from typing import cast -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.target_types import ConsoleScript +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.engine.rules import collect_rules, rule from pants.engine.unions import UnionRule from pants.option.custom_types import shell_str @@ -63,7 +65,7 @@ def args(self) -> tuple[str, ...]: return tuple(self.options.args) -class PyUpgradeLockfileSentinel(PythonToolLockfileSentinel): +class PyUpgradeLockfileSentinel(ToolLockfileSentinel): options_scope = PyUpgrade.options_scope @@ -75,4 +77,8 @@ def setup_pyupgrade_lockfile( def rules(): - return (*collect_rules(), UnionRule(PythonToolLockfileSentinel, PyUpgradeLockfileSentinel)) + return ( + *collect_rules(), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, PyUpgradeLockfileSentinel), + ) diff --git a/src/python/pants/backend/python/lint/yapf/subsystem.py b/src/python/pants/backend/python/lint/yapf/subsystem.py index 7538cac3cfc..d9faf23d8fb 100644 --- a/src/python/pants/backend/python/lint/yapf/subsystem.py +++ b/src/python/pants/backend/python/lint/yapf/subsystem.py @@ -6,9 +6,11 @@ import os.path from typing import Iterable, cast -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.target_types import ConsoleScript +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.core.util_rules.config_files import ConfigFilesRequest from pants.engine.rules import collect_rules, rule from pants.engine.unions import UnionRule @@ -116,7 +118,7 @@ def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest: ) -class YapfLockfileSentinel(PythonToolLockfileSentinel): +class YapfLockfileSentinel(ToolLockfileSentinel): options_scope = Yapf.options_scope @@ -126,4 +128,8 @@ def setup_yapf_lockfile(_: YapfLockfileSentinel, yapf: Yapf) -> PythonLockfileRe def rules(): - return (*collect_rules(), UnionRule(PythonToolLockfileSentinel, YapfLockfileSentinel)) + return ( + *collect_rules(), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, YapfLockfileSentinel), + ) diff --git a/src/python/pants/backend/python/subsystems/ipython.py b/src/python/pants/backend/python/subsystems/ipython.py index f5030a09af5..fa006ba1455 100644 --- a/src/python/pants/backend/python/subsystems/ipython.py +++ b/src/python/pants/backend/python/subsystems/ipython.py @@ -5,11 +5,13 @@ import itertools -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.subsystems.setup import PythonSetup from pants.backend.python.target_types import ConsoleScript, InterpreterConstraintsField from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.engine.rules import Get, collect_rules, rule from pants.engine.target import AllTargets, AllTargetsRequest from pants.engine.unions import UnionRule @@ -46,7 +48,7 @@ def register_options(cls, register): ) -class IPythonLockfileSentinel(PythonToolLockfileSentinel): +class IPythonLockfileSentinel(ToolLockfileSentinel): options_scope = IPython.options_scope @@ -85,4 +87,8 @@ async def setup_ipython_lockfile( def rules(): - return (*collect_rules(), UnionRule(PythonToolLockfileSentinel, IPythonLockfileSentinel)) + return ( + *collect_rules(), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, IPythonLockfileSentinel), + ) diff --git a/src/python/pants/backend/python/subsystems/lambdex.py b/src/python/pants/backend/python/subsystems/lambdex.py index 4b70e49d620..b8d4a39ec6a 100644 --- a/src/python/pants/backend/python/subsystems/lambdex.py +++ b/src/python/pants/backend/python/subsystems/lambdex.py @@ -1,9 +1,11 @@ # Copyright 2019 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.target_types import ConsoleScript +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.engine.rules import collect_rules, rule from pants.engine.unions import UnionRule from pants.util.docutil import git_url @@ -28,7 +30,7 @@ class Lambdex(PythonToolBase): default_lockfile_url = git_url(default_lockfile_path) -class LambdexLockfileSentinel(PythonToolLockfileSentinel): +class LambdexLockfileSentinel(ToolLockfileSentinel): options_scope = Lambdex.options_scope @@ -38,4 +40,8 @@ def setup_lambdex_lockfile(_: LambdexLockfileSentinel, lambdex: Lambdex) -> Pyth def rules(): - return (*collect_rules(), UnionRule(PythonToolLockfileSentinel, LambdexLockfileSentinel)) + return ( + *collect_rules(), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, LambdexLockfileSentinel), + ) diff --git a/src/python/pants/backend/python/subsystems/pytest.py b/src/python/pants/backend/python/subsystems/pytest.py index 44869496745..0d3860df831 100644 --- a/src/python/pants/backend/python/subsystems/pytest.py +++ b/src/python/pants/backend/python/subsystems/pytest.py @@ -10,7 +10,8 @@ from packaging.utils import canonicalize_name as canonicalize_project_name -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.pip_requirement import PipRequirement from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.subsystems.setup import PythonSetup @@ -24,6 +25,7 @@ format_invalid_requirement_string_error, ) from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.core.goals.test import RuntimePackageDependenciesField, TestFieldSet from pants.core.util_rules.config_files import ConfigFilesRequest from pants.engine.rules import Get, MultiGet, collect_rules, rule @@ -202,7 +204,7 @@ def validate_pytest_cov_included(self) -> None: ) -class PytestLockfileSentinel(PythonToolLockfileSentinel): +class PytestLockfileSentinel(ToolLockfileSentinel): options_scope = PyTest.options_scope @@ -243,4 +245,8 @@ async def setup_pytest_lockfile( def rules(): - return (*collect_rules(), UnionRule(PythonToolLockfileSentinel, PytestLockfileSentinel)) + return ( + *collect_rules(), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, PytestLockfileSentinel), + ) 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 7facc71b372..2bbea322705 100644 --- a/src/python/pants/backend/python/subsystems/python_tool_base.py +++ b/src/python/pants/backend/python/subsystems/python_tool_base.py @@ -16,14 +16,12 @@ ToolCustomLockfile, ToolDefaultLockfile, ) +from pants.core.goals.generate_lockfiles import DEFAULT_TOOL_LOCKFILE, NO_TOOL_LOCKFILE from pants.engine.fs import FileContent from pants.option.errors import OptionsError from pants.option.subsystem import Subsystem from pants.util.ordered_set import FrozenOrderedSet -DEFAULT_TOOL_LOCKFILE = "" -NO_TOOL_LOCKFILE = "" - class PythonToolRequirementsBase(Subsystem): """Base class for subsystems that configure a set of requirements for a python tool.""" diff --git a/src/python/pants/backend/python/subsystems/setuptools.py b/src/python/pants/backend/python/subsystems/setuptools.py index 816b16d2969..6169a58544f 100644 --- a/src/python/pants/backend/python/subsystems/setuptools.py +++ b/src/python/pants/backend/python/subsystems/setuptools.py @@ -4,11 +4,13 @@ import itertools from dataclasses import dataclass -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.subsystems.python_tool_base import PythonToolRequirementsBase from pants.backend.python.subsystems.setup import PythonSetup from pants.backend.python.target_types import PythonProvidesField from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.core.goals.package import PackageFieldSet from pants.engine.rules import Get, MultiGet, collect_rules, rule from pants.engine.target import ( @@ -42,7 +44,7 @@ class Setuptools(PythonToolRequirementsBase): default_lockfile_url = git_url(default_lockfile_path) -class SetuptoolsLockfileSentinel(PythonToolLockfileSentinel): +class SetuptoolsLockfileSentinel(ToolLockfileSentinel): options_scope = Setuptools.options_scope @@ -74,4 +76,8 @@ async def setup_setuptools_lockfile( def rules(): - return (*collect_rules(), UnionRule(PythonToolLockfileSentinel, SetuptoolsLockfileSentinel)) + return ( + *collect_rules(), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, SetuptoolsLockfileSentinel), + ) diff --git a/src/python/pants/backend/python/subsystems/twine.py b/src/python/pants/backend/python/subsystems/twine.py index bc978275a9c..6bff2fbedd9 100644 --- a/src/python/pants/backend/python/subsystems/twine.py +++ b/src/python/pants/backend/python/subsystems/twine.py @@ -7,9 +7,11 @@ from pathlib import Path from typing import cast -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.target_types import ConsoleScript +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.core.util_rules.config_files import ConfigFilesRequest from pants.engine.fs import CreateDigest, FileContent from pants.engine.rules import collect_rules, rule @@ -127,7 +129,7 @@ def ca_certs_digest_request(self, default_ca_certs_path: str | None) -> CreateDi return CreateDigest((FileContent(chrooted_ca_certs_path, ca_certs_content),)) -class TwineLockfileSentinel(PythonToolLockfileSentinel): +class TwineLockfileSentinel(ToolLockfileSentinel): options_scope = TwineSubsystem.options_scope @@ -137,4 +139,8 @@ def setup_twine_lockfile(_: TwineLockfileSentinel, twine: TwineSubsystem) -> Pyt def rules(): - return (*collect_rules(), UnionRule(PythonToolLockfileSentinel, TwineLockfileSentinel)) + return ( + *collect_rules(), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, TwineLockfileSentinel), + ) diff --git a/src/python/pants/backend/python/target_types.py b/src/python/pants/backend/python/target_types.py index 622597340ae..b20a5700170 100644 --- a/src/python/pants/backend/python/target_types.py +++ b/src/python/pants/backend/python/target_types.py @@ -28,6 +28,7 @@ from pants.backend.python.macros.python_artifact import PythonArtifact from pants.backend.python.pip_requirement import PipRequirement from pants.backend.python.subsystems.setup import PythonSetup +from pants.core.goals.generate_lockfiles import UnrecognizedResolveNamesError from pants.core.goals.package import OutputPathField from pants.core.goals.run import RestartableField from pants.core.goals.test import RuntimePackageDependenciesField @@ -102,27 +103,6 @@ def value_or_global_default(self, python_setup: PythonSetup) -> Tuple[str, ...]: return python_setup.compatibility_or_constraints(self.value) -class UnrecognizedResolveNamesError(Exception): - def __init__( - self, - unrecognized_resolve_names: list[str], - all_valid_names: Iterable[str], - *, - description_of_origin: str, - ) -> None: - # TODO(#12314): maybe implement "Did you mean?" - if len(unrecognized_resolve_names) == 1: - unrecognized_str = unrecognized_resolve_names[0] - name_description = "name" - else: - unrecognized_str = str(sorted(unrecognized_resolve_names)) - name_description = "names" - super().__init__( - f"Unrecognized resolve {name_description} from {description_of_origin}: " - f"{unrecognized_str}\n\nAll valid resolve names: {sorted(all_valid_names)}" - ) - - class PythonResolveField(StringField, AsyncFieldMixin): alias = "experimental_resolve" required = False diff --git a/src/python/pants/backend/python/target_types_test.py b/src/python/pants/backend/python/target_types_test.py index 2c5fc36b78d..a1d9cb3af8d 100644 --- a/src/python/pants/backend/python/target_types_test.py +++ b/src/python/pants/backend/python/target_types_test.py @@ -37,7 +37,6 @@ ResolvedPexEntryPoint, ResolvePexEntryPointRequest, ResolvePythonDistributionEntryPointsRequest, - UnrecognizedResolveNamesError, normalize_module_mapping, parse_requirements_file, ) @@ -51,6 +50,7 @@ resolve_pex_entry_point, ) from pants.backend.python.util_rules import python_sources +from pants.core.goals.generate_lockfiles import UnrecognizedResolveNamesError from pants.engine.addresses import Address from pants.engine.internals.scheduler import ExecutionError from pants.engine.target import ( diff --git a/src/python/pants/backend/python/typecheck/mypy/subsystem.py b/src/python/pants/backend/python/typecheck/mypy/subsystem.py index e486869fa7f..8ff59448ec9 100644 --- a/src/python/pants/backend/python/typecheck/mypy/subsystem.py +++ b/src/python/pants/backend/python/typecheck/mypy/subsystem.py @@ -8,7 +8,8 @@ from dataclasses import dataclass from typing import Iterable, cast -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.subsystems.python_tool_base import PythonToolBase from pants.backend.python.subsystems.setup import PythonSetup from pants.backend.python.target_types import ( @@ -23,6 +24,7 @@ PythonSourceFiles, PythonSourceFilesRequest, ) +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.core.util_rules.config_files import ConfigFiles, ConfigFilesRequest from pants.engine.addresses import Addresses, UnparsedAddressInputs from pants.engine.fs import EMPTY_DIGEST, Digest, DigestContents, FileContent @@ -281,7 +283,7 @@ async def mypy_first_party_plugins( # -------------------------------------------------------------------------------------- -class MyPyLockfileSentinel(PythonToolLockfileSentinel): +class MyPyLockfileSentinel(ToolLockfileSentinel): options_scope = MyPy.options_scope @@ -320,4 +322,8 @@ async def setup_mypy_lockfile( def rules(): - return (*collect_rules(), UnionRule(PythonToolLockfileSentinel, MyPyLockfileSentinel)) + return ( + *collect_rules(), + *lockfile.rules(), + UnionRule(ToolLockfileSentinel, MyPyLockfileSentinel), + ) diff --git a/src/python/pants/backend/terraform/dependency_inference.py b/src/python/pants/backend/terraform/dependency_inference.py index 27c6b60d3eb..08b8dd784cb 100644 --- a/src/python/pants/backend/terraform/dependency_inference.py +++ b/src/python/pants/backend/terraform/dependency_inference.py @@ -6,12 +6,14 @@ from dataclasses import dataclass from pathlib import PurePath -from pants.backend.python.goals.lockfile import PythonLockfileRequest, PythonToolLockfileSentinel +from pants.backend.python.goals import lockfile +from pants.backend.python.goals.lockfile import PythonLockfileRequest from pants.backend.python.subsystems.python_tool_base import PythonToolRequirementsBase from pants.backend.python.target_types import EntryPoint from pants.backend.python.util_rules.pex import PexRequest, VenvPex, VenvPexProcess from pants.backend.terraform.target_types import TerraformModuleSourcesField from pants.base.specs import AddressSpecs, MaybeEmptySiblingAddresses +from pants.core.goals.generate_lockfiles import ToolLockfileSentinel from pants.engine.fs import CreateDigest, Digest, FileContent from pants.engine.internals.selectors import Get from pants.engine.process import Process, ProcessResult @@ -43,7 +45,7 @@ class TerraformHcl2Parser(PythonToolRequirementsBase): default_lockfile_url = git_url(default_lockfile_path) -class TerraformHcl2ParserLockfileSentinel(PythonToolLockfileSentinel): +class TerraformHcl2ParserLockfileSentinel(ToolLockfileSentinel): options_scope = TerraformHcl2Parser.options_scope @@ -150,6 +152,7 @@ async def infer_terraform_module_dependencies( def rules(): return [ *collect_rules(), + *lockfile.rules(), UnionRule(InferDependenciesRequest, InferTerraformModuleDependenciesRequest), - UnionRule(PythonToolLockfileSentinel, TerraformHcl2ParserLockfileSentinel), + UnionRule(ToolLockfileSentinel, TerraformHcl2ParserLockfileSentinel), ] diff --git a/src/python/pants/core/goals/generate_lockfiles.py b/src/python/pants/core/goals/generate_lockfiles.py new file mode 100644 index 00000000000..f7e7e1bd501 --- /dev/null +++ b/src/python/pants/core/goals/generate_lockfiles.py @@ -0,0 +1,328 @@ +# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import annotations + +import itertools +import logging +from dataclasses import dataclass +from typing import ClassVar, Iterable, Sequence, cast + +from pants.engine.collection import Collection +from pants.engine.fs import Digest, MergeDigests, Workspace +from pants.engine.goal import Goal, GoalSubsystem +from pants.engine.internals.selectors import Get, MultiGet +from pants.engine.rules import collect_rules, goal_rule +from pants.engine.unions import UnionMembership, union + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class Lockfile: + """The result of generating a lockfile for a particular resolve.""" + + digest: Digest + resolve_name: str + path: str + + +@union +@dataclass(frozen=True) +class LockfileRequest: + """A union base for generating ecosystem-specific lockfiles. + + Each language ecosystem should set up a subclass of `LockfileRequest`, like + `PythonLockfileRequest` and `CoursierLockfileRequest`, and register a union rule. They should + also set up a simple rule that goes from that class -> `WrappedLockfileRequest`. + + Subclasses will usually want to add additional properties, such as Python interpreter + constraints. + """ + + resolve_name: str + lockfile_dest: str + + +@dataclass(frozen=True) +class WrappedLockfileRequest: + request: LockfileRequest + + +@union +class ToolLockfileSentinel: + """Tools use this as an entry point to say how to generate their tool lockfile. + + Each language ecosystem should set up a union member of `LockfileRequest`, like + `PythonLockfileRequest`, as explained in that class's docstring. + + Then, each tool should subclass `ToolLockfileSentinel` and set up a rule that goes from the + subclass -> the language's lockfile request, e.g. BlackLockfileSentinel -> + PythonLockfileRequest. Register a union rule for the `ToolLockfileSentinel` subclass. + """ + + options_scope: ClassVar[str] + + +class UserLockfileRequests(Collection[LockfileRequest]): + """All user resolves for a particular language ecosystem to build. + + Each language ecosystem should set up a subclass of `RequestedUserResolveNames` (see its + docstring), and implement a rule going from that subclass -> UserLockfileRequests. Each element + in the returned `UserLockfileRequests` should be a subclass of `LockfileRequest`, like + `PythonLockfileRequest`. + """ + + +@union +class KnownUserResolveNamesRequest: + """A hook for a language ecosystem to declare which resolves it has defined. + + Each language ecosystem should set up a subclass and register it with a UnionRule. Implement a + rule that goes from the subclass -> KnownUserResolveNames, usually by simply reading the + `resolves` option from the relevant subsystem. + """ + + +@dataclass(frozen=True) +class KnownUserResolveNames: + """All defined user resolves for a particular language ecosystem. + + See KnownUserResolveNamesRequest for how to use this type. `option_name` should be formatted + like `[options-scope].resolves` + """ + + names: tuple[str, ...] + option_name: str + requested_resolve_names_cls: type[RequestedUserResolveNames] + + +@union +class RequestedUserResolveNames(Collection[str]): + """The user resolves requested for a particular language ecosystem. + + Each language ecosystem should set up a subclass and register it with a UnionRule. Implement a + rule that goes from the subclass -> UserLockfileRequests. + """ + + +DEFAULT_TOOL_LOCKFILE = "" +NO_TOOL_LOCKFILE = "" + + +class UnrecognizedResolveNamesError(Exception): + def __init__( + self, + unrecognized_resolve_names: list[str], + all_valid_names: Iterable[str], + *, + description_of_origin: str, + ) -> None: + # TODO(#12314): maybe implement "Did you mean?" + if len(unrecognized_resolve_names) == 1: + unrecognized_str = unrecognized_resolve_names[0] + name_description = "name" + else: + unrecognized_str = str(sorted(unrecognized_resolve_names)) + name_description = "names" + super().__init__( + f"Unrecognized resolve {name_description} from {description_of_origin}: " + f"{unrecognized_str}\n\nAll valid resolve names: {sorted(all_valid_names)}" + ) + + +class AmbiguousResolveNamesError(Exception): + def __init__(self, ambiguous_names: list[str]) -> None: + if len(ambiguous_names) == 1: + first_paragraph = ( + "A resolve name from the option `[python].experimental_resolves` collides with the " + f"name of a tool resolve: {ambiguous_names[0]}" + ) + else: + first_paragraph = ( + "Some resolve names from the option `[python].experimental_resolves` collide with " + f"the names of tool resolves: {sorted(ambiguous_names)}" + ) + super().__init__( + f"{first_paragraph}\n\n" + "To fix, please update `[python].experimental_resolves` to use different resolve names." + ) + + +def determine_resolves_to_generate( + all_known_user_resolve_names: Iterable[KnownUserResolveNames], + all_tool_sentinels: Iterable[type[ToolLockfileSentinel]], + requested_resolve_names: set[str], +) -> tuple[list[RequestedUserResolveNames], list[type[ToolLockfileSentinel]]]: + """Apply the `--resolve` option to determine which resolves are specified. + + Return a tuple of `(user_resolves, tool_lockfile_sentinels)`. + """ + resolve_names_to_sentinels = { + sentinel.options_scope: sentinel for sentinel in all_tool_sentinels + } + + # TODO: check for ambiguity: between tools and user resolves, and across distinct + # `KnownUserResolveNames`s. Update AmbiguousResolveNamesError to say where the resolve + # name is defined, whereas right now we hardcode it to be the `[python]` option. + + if not requested_resolve_names: + return [ + known_resolve_names.requested_resolve_names_cls(known_resolve_names.names) + for known_resolve_names in all_known_user_resolve_names + ], list(all_tool_sentinels) + + requested_user_resolve_names = [] + for known_resolve_names in all_known_user_resolve_names: + requested = requested_resolve_names.intersection(known_resolve_names.names) + if requested: + requested_resolve_names -= requested + requested_user_resolve_names.append( + known_resolve_names.requested_resolve_names_cls(requested) + ) + + specified_sentinels = [] + for resolve, sentinel in resolve_names_to_sentinels.items(): + if resolve in requested_resolve_names: + requested_resolve_names.discard(resolve) + specified_sentinels.append(sentinel) + + if requested_resolve_names: + raise UnrecognizedResolveNamesError( + unrecognized_resolve_names=sorted(requested_resolve_names), + all_valid_names={ + *itertools.chain.from_iterable( + known_resolve_names.names + for known_resolve_names in all_known_user_resolve_names + ), + *resolve_names_to_sentinels.keys(), + }, + description_of_origin="the option `--generate-lockfiles-resolve`", + ) + + return requested_user_resolve_names, specified_sentinels + + +def filter_tool_lockfile_requests( + specified_requests: Sequence[WrappedLockfileRequest], *, resolve_specified: bool +) -> list[LockfileRequest]: + result = [] + for wrapped_req in specified_requests: + req = wrapped_req.request + if req.lockfile_dest not in (NO_TOOL_LOCKFILE, DEFAULT_TOOL_LOCKFILE): + result.append(req) + continue + if resolve_specified: + resolve = req.resolve_name + raise ValueError( + f"You requested to generate a lockfile for {resolve} because " + "you included it in `--generate-lockfiles-resolve`, but " + f"`[{resolve}].lockfile` is set to `{req.lockfile_dest}` " + "so a lockfile will not be generated.\n\n" + f"If you would like to generate a lockfile for {resolve}, please " + f"set `[{resolve}].lockfile` to the path where it should be " + "generated and run again." + ) + + return result + + +class GenerateLockfilesSubsystem(GoalSubsystem): + name = "generate-lockfiles" + help = "Generate lockfiles for Python third-party dependencies." + required_union_implementations = (ToolLockfileSentinel, KnownUserResolveNames) + + @classmethod + def register_options(cls, register) -> None: + super().register_options(register) + register( + "--resolve", + type=list, + member_type=str, + advanced=False, + help=( + "Only generate lockfiles for the specified resolve(s).\n\n" + "Resolves are the logical names for the different lockfiles used in your project. " + "For your own code's dependencies, these come from the option " + "`[python].experimental_resolves`. For tool lockfiles, resolve " + "names are the options scope for that tool such as `black`, `pytest`, and " + "`mypy-protobuf`.\n\n" + "For example, you can run `./pants generate-lockfiles --resolve=black " + "--resolve=pytest --resolve=data-science` to only generate lockfiles for those " + "two tools and your resolve named `data-science`.\n\n" + "If you specify an invalid resolve name, like 'fake', Pants will output all " + "possible values.\n\n" + "If not specified, Pants will generate lockfiles for all resolves." + ), + ) + register( + "--custom-command", + advanced=True, + type=str, + default=None, + help=( + "If set, lockfile headers will say to run this command to regenerate the lockfile, " + "rather than running `./pants generate-lockfiles --resolve=` like normal." + ), + ) + + @property + def resolve_names(self) -> tuple[str, ...]: + return tuple(self.options.resolve) + + @property + def custom_command(self) -> str | None: + return cast("str | None", self.options.custom_command) + + +class GenerateLockfilesGoal(Goal): + subsystem_cls = GenerateLockfilesSubsystem + + +@goal_rule +async def generate_lockfiles_goal( + workspace: Workspace, + union_membership: UnionMembership, + generate_lockfiles_subsystem: GenerateLockfilesSubsystem, +) -> GenerateLockfilesGoal: + known_user_resolve_names = await MultiGet( + Get(KnownUserResolveNames, KnownUserResolveNamesRequest, request()) + for request in union_membership.get(KnownUserResolveNamesRequest) + ) + requested_user_resolve_names, requested_tool_sentinels = determine_resolves_to_generate( + known_user_resolve_names, + union_membership.get(ToolLockfileSentinel), + set(generate_lockfiles_subsystem.resolve_names), + ) + + all_specified_user_requests = await MultiGet( + Get(UserLockfileRequests, RequestedUserResolveNames, resolve_names) + for resolve_names in requested_user_resolve_names + ) + specified_tool_requests = await MultiGet( + Get(WrappedLockfileRequest, ToolLockfileSentinel, sentinel()) + for sentinel in requested_tool_sentinels + ) + applicable_tool_requests = filter_tool_lockfile_requests( + specified_tool_requests, + resolve_specified=bool(generate_lockfiles_subsystem.resolve_names), + ) + + results = await MultiGet( + Get(Lockfile, LockfileRequest, req) + for req in ( + *(req for reqs in all_specified_user_requests for req in reqs), + *applicable_tool_requests, + ) + ) + + merged_digest = await Get(Digest, MergeDigests(res.digest for res in results)) + workspace.write_digest(merged_digest) + for result in results: + logger.info(f"Wrote lockfile for the resolve `{result.resolve_name}` to {result.path}") + + return GenerateLockfilesGoal(exit_code=0) + + +def rules(): + return collect_rules() diff --git a/src/python/pants/core/goals/generate_lockfiles_test.py b/src/python/pants/core/goals/generate_lockfiles_test.py new file mode 100644 index 00000000000..d15bd178c1f --- /dev/null +++ b/src/python/pants/core/goals/generate_lockfiles_test.py @@ -0,0 +1,125 @@ +# 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.core.goals.generate_lockfiles import ( + DEFAULT_TOOL_LOCKFILE, + NO_TOOL_LOCKFILE, + KnownUserResolveNames, + LockfileRequest, + RequestedUserResolveNames, + ToolLockfileSentinel, + UnrecognizedResolveNamesError, + WrappedLockfileRequest, + determine_resolves_to_generate, + filter_tool_lockfile_requests, +) + + +def test_determine_tool_sentinels_to_generate() -> None: + class Tool1(ToolLockfileSentinel): + options_scope = "tool1" + + class Tool2(ToolLockfileSentinel): + options_scope = "tool2" + + class Tool3(ToolLockfileSentinel): + options_scope = "tool3" + + class Lang1Requested(RequestedUserResolveNames): + pass + + class Lang2Requested(RequestedUserResolveNames): + pass + + lang1_resolves = KnownUserResolveNames( + ("u1", "u2"), option_name="[lang1].resolves", requested_resolve_names_cls=Lang1Requested + ) + lang2_resolves = KnownUserResolveNames( + ("u3",), option_name="[lang2].resolves", requested_resolve_names_cls=Lang2Requested + ) + + def assert_chosen( + requested: set[str], + expected_user_resolves: list[RequestedUserResolveNames], + expected_tools: list[type[ToolLockfileSentinel]], + ) -> None: + user_resolves, tools = determine_resolves_to_generate( + [lang1_resolves, lang2_resolves], [Tool1, Tool2, Tool3], requested + ) + assert user_resolves == expected_user_resolves + assert tools == expected_tools + + assert_chosen( + {Tool2.options_scope, "u2"}, + expected_user_resolves=[Lang1Requested(["u2"])], + expected_tools=[Tool2], + ) + assert_chosen( + {Tool1.options_scope, Tool3.options_scope}, + expected_user_resolves=[], + expected_tools=[Tool1, Tool3], + ) + + # If none are specifically requested, return all. + assert_chosen( + set(), + expected_user_resolves=[Lang1Requested(["u1", "u2"]), Lang2Requested(["u3"])], + expected_tools=[Tool1, Tool2, Tool3], + ) + + with pytest.raises(UnrecognizedResolveNamesError): + assert_chosen({"fake"}, expected_user_resolves=[], expected_tools=[]) + + # TODO: Add ambiguity checks. + # Error if same resolve name used for tool lockfiles and user lockfiles. + # class AmbiguousTool(ToolLockfileSentinel): + # options_scope = "ambiguous" + # + # with pytest.raises(AmbiguousResolveNamesError): + # determine_resolves_to_generate( + # {"ambiguous": "lockfile.txt"}, [AmbiguousTool], ["ambiguous"] + # ) + + +def test_filter_tool_lockfile_requests() -> None: + def create_request(name: str, lockfile_dest: str | None = None) -> LockfileRequest: + return LockfileRequest(resolve_name=name, lockfile_dest=lockfile_dest or f"{name}.txt") + + tool1 = create_request("tool1") + tool2 = create_request("tool2") + disabled_tool = create_request("none", lockfile_dest=NO_TOOL_LOCKFILE) + default_tool = create_request("default", lockfile_dest=DEFAULT_TOOL_LOCKFILE) + + def assert_filtered( + extra_request: LockfileRequest | None, + *, + resolve_specified: bool, + ) -> None: + requests = [WrappedLockfileRequest(tool1), WrappedLockfileRequest(tool2)] + if extra_request: + requests.append(WrappedLockfileRequest(extra_request)) + assert filter_tool_lockfile_requests(requests, resolve_specified=resolve_specified) == [ + tool1, + tool2, + ] + + assert_filtered(None, resolve_specified=False) + assert_filtered(None, resolve_specified=True) + + assert_filtered(disabled_tool, resolve_specified=False) + with pytest.raises(ValueError) as exc: + assert_filtered(disabled_tool, resolve_specified=True) + assert f"`[{disabled_tool.resolve_name}].lockfile` is set to `{NO_TOOL_LOCKFILE}`" in str( + exc.value + ) + + assert_filtered(default_tool, resolve_specified=False) + with pytest.raises(ValueError) as exc: + assert_filtered(default_tool, resolve_specified=True) + assert f"`[{default_tool.resolve_name}].lockfile` is set to `{DEFAULT_TOOL_LOCKFILE}`" in str( + exc.value + ) diff --git a/src/python/pants/core/register.py b/src/python/pants/core/register.py index 09f19d57c73..41fce4f1a07 100644 --- a/src/python/pants/core/register.py +++ b/src/python/pants/core/register.py @@ -10,6 +10,7 @@ check, export, fmt, + generate_lockfiles, lint, package, publish, @@ -50,6 +51,7 @@ def rules(): *check.rules(), *export.rules(), *fmt.rules(), + *generate_lockfiles.rules(), *lint.rules(), *update_build_files.rules(), *package.rules(), diff --git a/src/python/pants/jvm/resolve/jvm_tool.py b/src/python/pants/jvm/resolve/jvm_tool.py index 12a7fcaf515..9a6df8e05fb 100644 --- a/src/python/pants/jvm/resolve/jvm_tool.py +++ b/src/python/pants/jvm/resolve/jvm_tool.py @@ -8,8 +8,8 @@ from dataclasses import dataclass from typing import ClassVar, Iterable, Sequence, cast -from pants.backend.python.target_types import UnrecognizedResolveNamesError from pants.build_graph.address import Address, AddressInput +from pants.core.goals.generate_lockfiles import UnrecognizedResolveNamesError from pants.engine.addresses import Addresses from pants.engine.fs import ( CreateDigest, diff --git a/src/python/pants/jvm/resolve/jvm_tool_test.py b/src/python/pants/jvm/resolve/jvm_tool_test.py index ce7acde1b6e..9ff30fc7e35 100644 --- a/src/python/pants/jvm/resolve/jvm_tool_test.py +++ b/src/python/pants/jvm/resolve/jvm_tool_test.py @@ -7,8 +7,7 @@ import pytest -from pants.backend.python.subsystems.python_tool_base import DEFAULT_TOOL_LOCKFILE -from pants.backend.python.target_types import UnrecognizedResolveNamesError +from pants.core.goals.generate_lockfiles import DEFAULT_TOOL_LOCKFILE, UnrecognizedResolveNamesError from pants.core.util_rules import config_files, source_files from pants.core.util_rules.external_tool import rules as external_tool_rules from pants.engine.fs import Digest, DigestContents