From 6b6aa529fc07ef5c4be713bbd701aa898b37a1e9 Mon Sep 17 00:00:00 2001 From: Eric Arellano Date: Wed, 9 Feb 2022 13:04:51 -0700 Subject: [PATCH 1/6] An `export` plugin implementation can return multiple results This allows us to partition, e.g. one venv per resolve. # 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/goals/export.py | 13 ++++---- .../python/goals/export_integration_test.py | 11 ++++--- src/python/pants/core/goals/export.py | 33 +++++++++++-------- src/python/pants/core/goals/export_test.py | 25 +++++++------- 4 files changed, 45 insertions(+), 37 deletions(-) diff --git a/src/python/pants/backend/python/goals/export.py b/src/python/pants/backend/python/goals/export.py index d018e2f9642..880bb518715 100644 --- a/src/python/pants/backend/python/goals/export.py +++ b/src/python/pants/backend/python/goals/export.py @@ -11,7 +11,7 @@ from pants.backend.python.util_rules.pex import VenvPex, VenvPexProcess from pants.backend.python.util_rules.pex_environment import PexEnvironment from pants.backend.python.util_rules.pex_from_targets import RequirementsPexRequest -from pants.core.goals.export import ExportableData, ExportableDataRequest, ExportError, Symlink +from pants.core.goals.export import ExportError, ExportRequest, ExportResult, ExportResults, Symlink from pants.engine.internals.selectors import Get from pants.engine.process import ProcessResult from pants.engine.rules import collect_rules, rule @@ -19,14 +19,14 @@ @dataclass(frozen=True) -class ExportedVenvRequest(ExportableDataRequest): +class ExportVenvRequest(ExportRequest): pass @rule async def export_venv( - request: ExportedVenvRequest, python_setup: PythonSetup, pex_env: PexEnvironment -) -> ExportableData: + request: ExportVenvRequest, python_setup: PythonSetup, pex_env: PexEnvironment +) -> ExportResults: # Pick a single interpreter for the venv. interpreter_constraints = InterpreterConstraints.create_from_targets( request.targets, python_setup @@ -68,15 +68,16 @@ async def export_venv( ) py_version = res.stdout.strip().decode() - return ExportableData( + result = ExportResult( f"virtualenv for {min_interpreter}", os.path.join("python", "virtualenv"), symlinks=[Symlink(venv_abspath, py_version)], ) + return ExportResults((result,)) def rules(): return [ *collect_rules(), - UnionRule(ExportableDataRequest, ExportedVenvRequest), + UnionRule(ExportRequest, ExportVenvRequest), ] diff --git a/src/python/pants/backend/python/goals/export_integration_test.py b/src/python/pants/backend/python/goals/export_integration_test.py index 4883016b08c..0c77bd4d106 100644 --- a/src/python/pants/backend/python/goals/export_integration_test.py +++ b/src/python/pants/backend/python/goals/export_integration_test.py @@ -6,11 +6,11 @@ from pants.backend.python import target_types_rules from pants.backend.python.goals import export -from pants.backend.python.goals.export import ExportedVenvRequest +from pants.backend.python.goals.export import ExportVenvRequest from pants.backend.python.target_types import PythonRequirementTarget from pants.backend.python.util_rules import pex_from_targets from pants.base.specs import AddressSpecs, DescendantAddresses -from pants.core.goals.export import ExportableData +from pants.core.goals.export import ExportResults from pants.engine.rules import QueryRule from pants.engine.target import Targets from pants.testutil.rule_runner import RuleRunner @@ -24,7 +24,7 @@ def rule_runner() -> RuleRunner: *pex_from_targets.rules(), *target_types_rules.rules(), QueryRule(Targets, [AddressSpecs]), - QueryRule(ExportableData, [ExportedVenvRequest]), + QueryRule(ExportResults, [ExportVenvRequest]), ], target_types=[PythonRequirementTarget], ) @@ -43,7 +43,10 @@ def test_export_venv(rule_runner: RuleRunner) -> None: {"src/foo/BUILD": "python_requirement(name='req', requirements=['ansicolors==1.1.8'])"} ) targets = rule_runner.request(Targets, [AddressSpecs([DescendantAddresses("src/foo")])]) - data = rule_runner.request(ExportableData, [ExportedVenvRequest(targets)]) + all_results = rule_runner.request(ExportResults, [ExportVenvRequest(targets)]) + assert len(all_results) == 1 + data = all_results[0] + assert len(data.symlinks) == 1 symlink = data.symlinks[0] assert symlink.link_rel_path == current_interpreter diff --git a/src/python/pants/core/goals/export.py b/src/python/pants/core/goals/export.py index 4146653b6a9..18e796c90df 100644 --- a/src/python/pants/core/goals/export.py +++ b/src/python/pants/core/goals/export.py @@ -9,6 +9,7 @@ from pants.base.build_root import BuildRoot from pants.core.util_rules.distdir import DistDir +from pants.engine.collection import Collection from pants.engine.console import Console from pants.engine.fs import EMPTY_DIGEST, AddPrefix, Digest, MergeDigests, Workspace from pants.engine.goal import Goal, GoalSubsystem @@ -26,7 +27,7 @@ class ExportError(Exception): @union @dataclass(frozen=True) -class ExportableDataRequest: +class ExportRequest: """A union for exportable data provided by a backend. Subclass and install a member of this type to export data. @@ -41,7 +42,7 @@ class Symlink: source_path may be absolute, or relative to the repo root. - link_rel_path is relative to the enclosing ExportableData's reldir, and will be + link_rel_path is relative to the enclosing ExportResult's reldir, and will be absolutized when a location for that dir is chosen. """ @@ -51,7 +52,7 @@ class Symlink: @frozen_after_init @dataclass(unsafe_hash=True) -class ExportableData: +class ExportResult: description: str # Materialize digests and create symlinks under this reldir. reldir: str @@ -75,6 +76,10 @@ def __init__( self.symlinks = tuple(symlinks) +class ExportResults(Collection[ExportResult]): + pass + + class ExportSubsystem(GoalSubsystem): name = "export" help = "Export Pants data for use in other tools, such as IDEs." @@ -94,29 +99,29 @@ async def export( build_root: BuildRoot, dist_dir: DistDir, ) -> Export: - request_types = cast( - "Iterable[type[ExportableDataRequest]]", union_membership.get(ExportableDataRequest) - ) + request_types = cast("Iterable[type[ExportRequest]]", union_membership.get(ExportRequest)) requests = tuple(request_type(targets) for request_type in request_types) - exportables = await MultiGet( - Get(ExportableData, ExportableDataRequest, request) for request in requests - ) + all_results = await MultiGet(Get(ExportResults, ExportRequest, request) for request in requests) + flattened_results = [res for results in all_results for res in results] + prefixed_digests = await MultiGet( - Get(Digest, AddPrefix(exp.digest, exp.reldir)) for exp in exportables + Get(Digest, AddPrefix(result.digest, result.reldir)) for result in flattened_results ) output_dir = os.path.join(str(dist_dir.relpath), "export") merged_digest = await Get(Digest, MergeDigests(prefixed_digests)) dist_digest = await Get(Digest, AddPrefix(merged_digest, output_dir)) workspace.write_digest(dist_digest) - for exp in exportables: - for symlink in exp.symlinks: + for result in flattened_results: + for symlink in result.symlinks: # Note that if symlink.source_path is an abspath, join returns it unchanged. source_abspath = os.path.join(build_root.path, symlink.source_path) link_abspath = os.path.abspath( - os.path.join(output_dir, exp.reldir, symlink.link_rel_path) + os.path.join(output_dir, result.reldir, symlink.link_rel_path) ) absolute_symlink(source_abspath, link_abspath) - console.print_stdout(f"Wrote {exp.description} to {os.path.join(output_dir, exp.reldir)}") + console.print_stdout( + f"Wrote {result.description} to {os.path.join(output_dir, result.reldir)}" + ) return Export(exit_code=0) diff --git a/src/python/pants/core/goals/export_test.py b/src/python/pants/core/goals/export_test.py index ac9315b46af..4feb7da9979 100644 --- a/src/python/pants/core/goals/export_test.py +++ b/src/python/pants/core/goals/export_test.py @@ -10,8 +10,9 @@ from pants.base.build_root import BuildRoot from pants.core.goals.export import ( Export, - ExportableData, - ExportableDataRequest, + ExportRequest, + ExportResult, + ExportResults, ExportSubsystem, Symlink, export, @@ -35,14 +36,12 @@ def make_target(path: str, target_name: str) -> Target: return MockTarget({}, Address(path, target_name=target_name)) -class MockExportableDataRequest(ExportableDataRequest): +class MockExportRequest(ExportRequest): pass -def mock_export( - edr: ExportableDataRequest, digest: Digest, symlinks: tuple[Symlink, ...] -) -> ExportableData: - return ExportableData( +def mock_export(edr: ExportRequest, digest: Digest, symlinks: tuple[Symlink, ...]) -> ExportResult: + return ExportResult( description=f"mock export for {','.join(t.address.spec for t in edr.targets)}", reldir="mock", digest=digest, @@ -51,7 +50,7 @@ def mock_export( def run_export_rule(rule_runner: RuleRunner, targets: List[Target]) -> Tuple[int, str]: - union_membership = UnionMembership({ExportableDataRequest: [MockExportableDataRequest]}) + union_membership = UnionMembership({ExportRequest: [MockExportRequest]}) with open(os.path.join(rule_runner.build_root, "somefile"), "wb") as fp: fp.write(b"SOMEFILE") with mock_console(create_options_bootstrapper()) as (console, stdio_reader): @@ -69,10 +68,10 @@ def run_export_rule(rule_runner: RuleRunner, targets: List[Target]) -> Tuple[int ], mock_gets=[ MockGet( - output_type=ExportableData, - input_type=ExportableDataRequest, - mock=lambda edr: mock_export( - edr, digest, (Symlink("somefile", "link_to_somefile"),) + output_type=ExportResults, + input_type=ExportRequest, + mock=lambda req: ExportResults( + (mock_export(req, digest, (Symlink("somefile", "link_to_somefile"),)),), ), ), MockGet( @@ -94,7 +93,7 @@ def run_export_rule(rule_runner: RuleRunner, targets: List[Target]) -> Tuple[int def test_run_export_rule() -> None: rule_runner = RuleRunner( rules=[ - UnionRule(ExportableDataRequest, MockExportableDataRequest), + UnionRule(ExportRequest, MockExportRequest), QueryRule(Digest, [CreateDigest]), ], target_types=[MockTarget], From 490504ff15f6ba216ec8511ad147822c8d054dcd Mon Sep 17 00:00:00 2001 From: Eric Arellano Date: Thu, 10 Feb 2022 15:01:55 -0700 Subject: [PATCH 2/6] Update rule to generate one venv per resolve # 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/goals/export.py | 91 ++++++++++++++----- .../python/goals/export_integration_test.py | 47 +++++++--- src/python/pants/core/goals/export.py | 1 - 3 files changed, 104 insertions(+), 35 deletions(-) diff --git a/src/python/pants/backend/python/goals/export.py b/src/python/pants/backend/python/goals/export.py index 880bb518715..81884b0c522 100644 --- a/src/python/pants/backend/python/goals/export.py +++ b/src/python/pants/backend/python/goals/export.py @@ -3,50 +3,67 @@ from __future__ import annotations +import logging import os +from collections import defaultdict from dataclasses import dataclass +from typing import DefaultDict from pants.backend.python.subsystems.setup import PythonSetup +from pants.backend.python.target_types import PythonResolveField from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints from pants.backend.python.util_rules.pex import VenvPex, VenvPexProcess from pants.backend.python.util_rules.pex_environment import PexEnvironment from pants.backend.python.util_rules.pex_from_targets import RequirementsPexRequest from pants.core.goals.export import ExportError, ExportRequest, ExportResult, ExportResults, Symlink -from pants.engine.internals.selectors import Get +from pants.core.util_rules.distdir import DistDir +from pants.engine.engine_aware import EngineAwareParameter +from pants.engine.internals.selectors import Get, MultiGet from pants.engine.process import ProcessResult from pants.engine.rules import collect_rules, rule +from pants.engine.target import Target from pants.engine.unions import UnionRule +from pants.util.docutil import bin_name +from pants.util.strutil import path_safe + +logger = logging.getLogger(__name__) @dataclass(frozen=True) -class ExportVenvRequest(ExportRequest): +class ExportVenvsRequest(ExportRequest): pass +@dataclass(frozen=True) +class _ExportVenvRequest(EngineAwareParameter): + resolve: str + root_python_targets: tuple[Target, ...] + + def debug_hint(self) -> str: + return self.resolve + + @rule -async def export_venv( - request: ExportVenvRequest, python_setup: PythonSetup, pex_env: PexEnvironment -) -> ExportResults: - # Pick a single interpreter for the venv. - interpreter_constraints = InterpreterConstraints.create_from_targets( - request.targets, python_setup +async def export_virtualenv( + request: _ExportVenvRequest, python_setup: PythonSetup, pex_env: PexEnvironment +) -> ExportResult: + interpreter_constraints = InterpreterConstraints( + python_setup.resolves_to_interpreter_constraints.get( + request.resolve, python_setup.interpreter_constraints + ) ) - if not interpreter_constraints: - # If there were no targets that defined any constraints, fall back to the global ones. - interpreter_constraints = InterpreterConstraints(python_setup.interpreter_constraints) min_interpreter = interpreter_constraints.snap_to_minimum(python_setup.interpreter_universe) if not min_interpreter: raise ExportError( - "The following interpreter constraints were computed for all the targets for which " - f"export was requested: {interpreter_constraints}. There is no python interpreter " - "compatible with these constraints. Please restrict the target set to one that shares " - "a compatible interpreter." + f"The resolve '{request.resolve}' (from `[python].resolves`) has invalid interpreter " + f"constraints, which are set via `[python].resolves_to_interpreter_constraints`: " + f"{interpreter_constraints}. Could not determine the minimum compatible interpreter." ) venv_pex = await Get( VenvPex, RequirementsPexRequest( - (tgt.address for tgt in request.targets), + (tgt.address for tgt in request.root_python_targets), internal_only=True, hardcoded_interpreter_constraints=min_interpreter, ), @@ -68,16 +85,48 @@ async def export_venv( ) py_version = res.stdout.strip().decode() - result = ExportResult( - f"virtualenv for {min_interpreter}", - os.path.join("python", "virtualenv"), + return ExportResult( + f"virtualenv for the resolve '{request.resolve}' (using {min_interpreter})", + os.path.join("python", "virtualenvs", path_safe(request.resolve)), symlinks=[Symlink(venv_abspath, py_version)], ) - return ExportResults((result,)) + + +@rule +async def export_virtualenvs( + request: ExportVenvsRequest, python_setup: PythonSetup, dist_dir: DistDir +) -> ExportResults: + resolve_to_root_targets: DefaultDict[str, list[Target]] = defaultdict(list) + for tgt in request.targets: + if not tgt.has_field(PythonResolveField): + continue + resolve = tgt[PythonResolveField].normalized_value(python_setup) + resolve_to_root_targets[resolve].append(tgt) + + venvs = await MultiGet( + Get( + ExportResult, + _ExportVenvRequest( + resolve if resolve != "" else python_setup.default_resolve, tuple(tgts) + ), + ) + for resolve, tgts in resolve_to_root_targets.items() + ) + + deprecated_path = dist_dir.relpath / "python" / "virtualenv" + if venvs and deprecated_path.exists(): + logger.warning( + f"`{bin_name()} export ::` no longer writes virtualenvs to {deprecated_path}, but " + f"instead underneath {dist_dir.relpath / 'python' / 'virtualenvs'}. You will need to " + "update your IDE to point to the new virtualenv.\n\n" + f"To silence this error, delete {deprecated_path}" + ) + + return ExportResults(venvs) def rules(): return [ *collect_rules(), - UnionRule(ExportRequest, ExportVenvRequest), + UnionRule(ExportRequest, ExportVenvsRequest), ] diff --git a/src/python/pants/backend/python/goals/export_integration_test.py b/src/python/pants/backend/python/goals/export_integration_test.py index 0c77bd4d106..02a497b2573 100644 --- a/src/python/pants/backend/python/goals/export_integration_test.py +++ b/src/python/pants/backend/python/goals/export_integration_test.py @@ -1,16 +1,18 @@ # Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). import sys +from textwrap import dedent import pytest from pants.backend.python import target_types_rules from pants.backend.python.goals import export -from pants.backend.python.goals.export import ExportVenvRequest +from pants.backend.python.goals.export import ExportVenvsRequest from pants.backend.python.target_types import PythonRequirementTarget from pants.backend.python.util_rules import pex_from_targets from pants.base.specs import AddressSpecs, DescendantAddresses from pants.core.goals.export import ExportResults +from pants.core.util_rules import distdir from pants.engine.rules import QueryRule from pants.engine.target import Targets from pants.testutil.rule_runner import RuleRunner @@ -23,31 +25,50 @@ def rule_runner() -> RuleRunner: *export.rules(), *pex_from_targets.rules(), *target_types_rules.rules(), + *distdir.rules(), QueryRule(Targets, [AddressSpecs]), - QueryRule(ExportResults, [ExportVenvRequest]), + QueryRule(ExportResults, [ExportVenvsRequest]), ], target_types=[PythonRequirementTarget], ) -def test_export_venv(rule_runner: RuleRunner) -> None: +def test_export_venvs(rule_runner: RuleRunner) -> None: # We know that the current interpreter exists on the system. vinfo = sys.version_info current_interpreter = f"{vinfo.major}.{vinfo.minor}.{vinfo.micro}" rule_runner.set_options( - [f"--python-interpreter-constraints=['=={current_interpreter}']"], + [ + f"--python-interpreter-constraints=['=={current_interpreter}']", + "--python-resolves={'a': 'lock.txt', 'b': 'lock.txt'}", + "--python-enable-resolves", + # Turn off lockfile validation to make the test simpler. + "--python-invalid-lockfile-behavior=ignore", + ], env_inherit={"PATH", "PYENV_ROOT"}, ) rule_runner.write_files( - {"src/foo/BUILD": "python_requirement(name='req', requirements=['ansicolors==1.1.8'])"} + { + "src/foo/BUILD": dedent( + """\ + python_requirement(name='req1', requirements=['ansicolors==1.1.8'], resolve='a') + python_requirement(name='req2', requirements=['ansicolors==1.1.8'], resolve='b') + """ + ), + "lock.txt": "ansicolors==1.1.8", + } ) targets = rule_runner.request(Targets, [AddressSpecs([DescendantAddresses("src/foo")])]) - all_results = rule_runner.request(ExportResults, [ExportVenvRequest(targets)]) - assert len(all_results) == 1 - data = all_results[0] - - assert len(data.symlinks) == 1 - symlink = data.symlinks[0] - assert symlink.link_rel_path == current_interpreter - assert "named_caches/pex_root/venvs/" in symlink.source_path + all_results = rule_runner.request(ExportResults, [ExportVenvsRequest(targets)]) + assert len(all_results) == 2 + assert {result.reldir for result in all_results} == { + "python/virtualenvs/a", + "python/virtualenvs/b", + } + + for result in all_results: + assert len(result.symlinks) == 1 + symlink = result.symlinks[0] + assert symlink.link_rel_path == current_interpreter + assert "named_caches/pex_root/venvs/" in symlink.source_path diff --git a/src/python/pants/core/goals/export.py b/src/python/pants/core/goals/export.py index 18e796c90df..c64c91e8539 100644 --- a/src/python/pants/core/goals/export.py +++ b/src/python/pants/core/goals/export.py @@ -93,7 +93,6 @@ class Export(Goal): async def export( console: Console, targets: Targets, - export_subsystem: ExportSubsystem, workspace: Workspace, union_membership: UnionMembership, build_root: BuildRoot, From aab881007ff37d9efa20dd0cac0acee21554cea0 Mon Sep 17 00:00:00 2001 From: Eric Arellano Date: Thu, 10 Feb 2022 15:19:57 -0700 Subject: [PATCH 3/6] Restore original functionality if `--no-enable-resolves` # 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/goals/export.py | 56 +++++++++++++------ .../python/goals/export_integration_test.py | 49 +++++++++------- 2 files changed, 67 insertions(+), 38 deletions(-) diff --git a/src/python/pants/backend/python/goals/export.py b/src/python/pants/backend/python/goals/export.py index 81884b0c522..0b2f8e8cfb5 100644 --- a/src/python/pants/backend/python/goals/export.py +++ b/src/python/pants/backend/python/goals/export.py @@ -36,7 +36,7 @@ class ExportVenvsRequest(ExportRequest): @dataclass(frozen=True) class _ExportVenvRequest(EngineAwareParameter): - resolve: str + resolve: str | None root_python_targets: tuple[Target, ...] def debug_hint(self) -> str: @@ -47,18 +47,34 @@ def debug_hint(self) -> str: async def export_virtualenv( request: _ExportVenvRequest, python_setup: PythonSetup, pex_env: PexEnvironment ) -> ExportResult: - interpreter_constraints = InterpreterConstraints( - python_setup.resolves_to_interpreter_constraints.get( - request.resolve, python_setup.interpreter_constraints + if request.resolve: + interpreter_constraints = InterpreterConstraints( + python_setup.resolves_to_interpreter_constraints.get( + request.resolve, python_setup.interpreter_constraints + ) ) - ) + else: + interpreter_constraints = InterpreterConstraints.create_from_targets( + request.root_python_targets, python_setup + ) or InterpreterConstraints(python_setup.interpreter_constraints) + min_interpreter = interpreter_constraints.snap_to_minimum(python_setup.interpreter_universe) if not min_interpreter: - raise ExportError( - f"The resolve '{request.resolve}' (from `[python].resolves`) has invalid interpreter " - f"constraints, which are set via `[python].resolves_to_interpreter_constraints`: " - f"{interpreter_constraints}. Could not determine the minimum compatible interpreter." + err_msg = ( + ( + f"The resolve '{request.resolve}' (from `[python].resolves`) has invalid interpreter " + f"constraints, which are set via `[python].resolves_to_interpreter_constraints`: " + f"{interpreter_constraints}. Could not determine the minimum compatible interpreter." + ) + if request.resolve + else ( + "The following interpreter constraints were computed for all the targets for which " + f"export was requested: {interpreter_constraints}. There is no python interpreter " + "compatible with these constraints. Please restrict the target set to one that shares " + "a compatible interpreter." + ) ) + raise ExportError(err_msg) venv_pex = await Get( VenvPex, @@ -85,9 +101,14 @@ async def export_virtualenv( ) py_version = res.stdout.strip().decode() + dest = ( + os.path.join("python", "virtualenvs", path_safe(request.resolve)) + if request.resolve + else os.path.join("python", "virtualenv") + ) return ExportResult( f"virtualenv for the resolve '{request.resolve}' (using {min_interpreter})", - os.path.join("python", "virtualenvs", path_safe(request.resolve)), + dest, symlinks=[Symlink(venv_abspath, py_version)], ) @@ -106,20 +127,19 @@ async def export_virtualenvs( venvs = await MultiGet( Get( ExportResult, - _ExportVenvRequest( - resolve if resolve != "" else python_setup.default_resolve, tuple(tgts) - ), + _ExportVenvRequest(resolve if python_setup.enable_resolves else None, tuple(tgts)), ) for resolve, tgts in resolve_to_root_targets.items() ) - deprecated_path = dist_dir.relpath / "python" / "virtualenv" - if venvs and deprecated_path.exists(): + no_resolves_dest = dist_dir.relpath / "python" / "virtualenv" + if venvs and python_setup.enable_resolves and no_resolves_dest.exists(): logger.warning( - f"`{bin_name()} export ::` no longer writes virtualenvs to {deprecated_path}, but " - f"instead underneath {dist_dir.relpath / 'python' / 'virtualenvs'}. You will need to " + f"Because `[python].enable_resolves` is true, `{bin_name()} export ::` no longer " + f"writes virtualenvs to {no_resolves_dest}, but instead underneath " + f"{dist_dir.relpath / 'python' / 'virtualenvs'}. You will need to " "update your IDE to point to the new virtualenv.\n\n" - f"To silence this error, delete {deprecated_path}" + f"To silence this error, delete {no_resolves_dest}" ) return ExportResults(venvs) diff --git a/src/python/pants/backend/python/goals/export_integration_test.py b/src/python/pants/backend/python/goals/export_integration_test.py index 02a497b2573..97ea18731b6 100644 --- a/src/python/pants/backend/python/goals/export_integration_test.py +++ b/src/python/pants/backend/python/goals/export_integration_test.py @@ -37,17 +37,6 @@ def test_export_venvs(rule_runner: RuleRunner) -> None: # We know that the current interpreter exists on the system. vinfo = sys.version_info current_interpreter = f"{vinfo.major}.{vinfo.minor}.{vinfo.micro}" - - rule_runner.set_options( - [ - f"--python-interpreter-constraints=['=={current_interpreter}']", - "--python-resolves={'a': 'lock.txt', 'b': 'lock.txt'}", - "--python-enable-resolves", - # Turn off lockfile validation to make the test simpler. - "--python-invalid-lockfile-behavior=ignore", - ], - env_inherit={"PATH", "PYENV_ROOT"}, - ) rule_runner.write_files( { "src/foo/BUILD": dedent( @@ -59,16 +48,36 @@ def test_export_venvs(rule_runner: RuleRunner) -> None: "lock.txt": "ansicolors==1.1.8", } ) - targets = rule_runner.request(Targets, [AddressSpecs([DescendantAddresses("src/foo")])]) - all_results = rule_runner.request(ExportResults, [ExportVenvsRequest(targets)]) - assert len(all_results) == 2 - assert {result.reldir for result in all_results} == { + + def run(enable_resolves: bool) -> ExportResults: + rule_runner.set_options( + [ + f"--python-interpreter-constraints=['=={current_interpreter}']", + "--python-resolves={'a': 'lock.txt', 'b': 'lock.txt'}", + f"--python-enable-resolves={enable_resolves}", + # Turn off lockfile validation to make the test simpler. + "--python-invalid-lockfile-behavior=ignore", + ], + env_inherit={"PATH", "PYENV_ROOT"}, + ) + targets = rule_runner.request(Targets, [AddressSpecs([DescendantAddresses("src/foo")])]) + all_results = rule_runner.request(ExportResults, [ExportVenvsRequest(targets)]) + + for result in all_results: + assert len(result.symlinks) == 1 + symlink = result.symlinks[0] + assert symlink.link_rel_path == current_interpreter + assert "named_caches/pex_root/venvs/" in symlink.source_path + + return all_results + + resolve_results = run(enable_resolves=True) + assert len(resolve_results) == 2 + assert {result.reldir for result in resolve_results} == { "python/virtualenvs/a", "python/virtualenvs/b", } - for result in all_results: - assert len(result.symlinks) == 1 - symlink = result.symlinks[0] - assert symlink.link_rel_path == current_interpreter - assert "named_caches/pex_root/venvs/" in symlink.source_path + no_resolve_results = run(enable_resolves=False) + assert len(no_resolve_results) == 1 + assert no_resolve_results[0].reldir == "python/virtualenv" From fb3b9a00034c31a2600cad7e766ec6cac8ad9942 Mon Sep 17 00:00:00 2001 From: Eric Arellano Date: Thu, 10 Feb 2022 15:40:40 -0700 Subject: [PATCH 4/6] Fix MyPy type hint # 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/goals/export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python/pants/backend/python/goals/export.py b/src/python/pants/backend/python/goals/export.py index 0b2f8e8cfb5..bff492d4562 100644 --- a/src/python/pants/backend/python/goals/export.py +++ b/src/python/pants/backend/python/goals/export.py @@ -39,7 +39,7 @@ class _ExportVenvRequest(EngineAwareParameter): resolve: str | None root_python_targets: tuple[Target, ...] - def debug_hint(self) -> str: + def debug_hint(self) -> str | None: return self.resolve From 30b0ed383ddb43db810325d45fc5d949b0e02d94 Mon Sep 17 00:00:00 2001 From: Eric Arellano Date: Thu, 10 Feb 2022 16:42:02 -0700 Subject: [PATCH 5/6] Fix unit test # 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/core/goals/export_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/python/pants/core/goals/export_test.py b/src/python/pants/core/goals/export_test.py index 4feb7da9979..4ba719bed67 100644 --- a/src/python/pants/core/goals/export_test.py +++ b/src/python/pants/core/goals/export_test.py @@ -23,7 +23,7 @@ from pants.engine.rules import QueryRule from pants.engine.target import Target, Targets from pants.engine.unions import UnionMembership, UnionRule -from pants.testutil.option_util import create_goal_subsystem, create_options_bootstrapper +from pants.testutil.option_util import create_options_bootstrapper from pants.testutil.rule_runner import MockGet, RuleRunner, mock_console, run_rule_with_mocks @@ -60,7 +60,6 @@ def run_export_rule(rule_runner: RuleRunner, targets: List[Target]) -> Tuple[int rule_args=[ console, Targets(targets), - create_goal_subsystem(ExportSubsystem), Workspace(rule_runner.scheduler, _enforce_effects=False), union_membership, BuildRoot(), From 4dd75ba62728a1d691cb5ce5e3b7cce922ad311c Mon Sep 17 00:00:00 2001 From: Eric Arellano Date: Thu, 10 Feb 2022 17:04:32 -0700 Subject: [PATCH 6/6] Unused import # 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/core/goals/export_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/python/pants/core/goals/export_test.py b/src/python/pants/core/goals/export_test.py index 4ba719bed67..8ae37fc557e 100644 --- a/src/python/pants/core/goals/export_test.py +++ b/src/python/pants/core/goals/export_test.py @@ -13,7 +13,6 @@ ExportRequest, ExportResult, ExportResults, - ExportSubsystem, Symlink, export, )