diff --git a/docs/notes/2.32.x.md b/docs/notes/2.32.x.md index a9d5d4f370e..c04a7e84e4e 100644 --- a/docs/notes/2.32.x.md +++ b/docs/notes/2.32.x.md @@ -149,6 +149,8 @@ The version of [Pex](https://github.com/pex-tool/pex) used by the Python backend When generating lockfiles, the new `python.resolves_to_uploaded_prior_to` option can be used to pass along [`--uploaded-prior-to`](https://pip.pypa.io/en/stable/user_guide/#filtering-by-upload-time). This allows you to filter packages by their upload time to an index, only considering packages that were uploaded before a specified point in time. +The `runtime_package_dependencies` of `python_test` now traverse generic `target`s used for dependency aggregation. + #### Shell #### Javascript diff --git a/src/python/pants/core/goals/test.py b/src/python/pants/core/goals/test.py index 0884885ee83..d90a68a0f84 100644 --- a/src/python/pants/core/goals/test.py +++ b/src/python/pants/core/goals/test.py @@ -30,6 +30,7 @@ environment_aware_package, ) from pants.core.subsystems.debug_adapter import DebugAdapterSubsystem +from pants.core.target_types import GenericTarget from pants.core.util_rules.distdir import DistDir from pants.core.util_rules.env_vars import environment_vars_subset from pants.core.util_rules.partitions import ( @@ -47,7 +48,7 @@ from pants.engine.env_vars import EXTRA_ENV_VARS_USAGE_HELP, EnvironmentVars, EnvironmentVarsRequest from pants.engine.fs import EMPTY_FILE_DIGEST, FileDigest, MergeDigests, Snapshot, Workspace from pants.engine.goal import Goal, GoalSubsystem -from pants.engine.internals.graph import find_valid_field_sets, resolve_targets +from pants.engine.internals.graph import find_valid_field_sets, resolve_targets, transitive_targets from pants.engine.internals.session import RunId from pants.engine.internals.specs_rules import find_valid_field_sets_for_target_roots from pants.engine.intrinsics import merge_digests, run_interactive_process_in_environment @@ -59,16 +60,21 @@ ) from pants.engine.rules import collect_rules, concurrently, goal_rule, implicitly, rule from pants.engine.target import ( + Dependencies, + DepsTraversalBehavior, FieldSet, FieldSetsPerTargetRequest, IntField, NoApplicableTargetsBehavior, + ShouldTraverseDepsPredicate, SourcesField, SpecialCasedDependencies, StringField, StringSequenceField, + Target, TargetRootsToFieldSets, TargetRootsToFieldSetsRequest, + TransitiveTargetsRequest, ValidNumbers, parse_shard_spec, ) @@ -1240,6 +1246,21 @@ class RuntimePackageDependenciesField(SpecialCasedDependencies): ) +class TraverseGenericTargetDepsOnly(ShouldTraverseDepsPredicate): + """Traverses deps of `target()` (GenericTarget) entries, stops at all other target types. + + Used to unwrap a `target()` alias that groups packageable targets, so that + `runtime_package_dependencies` can reference the alias instead of each target individually. + """ + + def __call__( + self, target: Target, field: Dependencies | SpecialCasedDependencies + ) -> DepsTraversalBehavior: + if isinstance(target, GenericTarget) and isinstance(field, Dependencies): + return DepsTraversalBehavior.INCLUDE + return DepsTraversalBehavior.EXCLUDE + + class BuiltPackageDependencies(Collection[BuiltPackage]): pass @@ -1257,8 +1278,22 @@ async def build_runtime_package_dependencies( if not unparsed_addresses: return BuiltPackageDependencies() tgts = await resolve_targets(**implicitly(unparsed_addresses)) + + # Unwrap GenericTarget ("target()") entries by traversing their deps transitively, + # stopping at non-GenericTarget targets. This lets callers group packageable targets + # under a single `target()` alias and reference that alias in + # runtime_package_dependencies, rather than listing each packageable target individually. + transitive = await transitive_targets( + TransitiveTargetsRequest( + [tgt.address for tgt in tgts], + should_traverse_deps_predicate=TraverseGenericTargetDepsOnly(), + ), + **implicitly(), + ) + non_generic = [tgt for tgt in transitive.closure if not isinstance(tgt, GenericTarget)] + field_sets_per_tgt = await find_valid_field_sets( - FieldSetsPerTargetRequest(PackageFieldSet, tgts), **implicitly() + FieldSetsPerTargetRequest(PackageFieldSet, non_generic), **implicitly() ) packages = await concurrently( environment_aware_package(EnvironmentAwarePackageRequest(field_set)) diff --git a/src/python/pants/core/goals/test_test.py b/src/python/pants/core/goals/test_test.py index ca7c721d11e..857cc9b7724 100644 --- a/src/python/pants/core/goals/test_test.py +++ b/src/python/pants/core/goals/test_test.py @@ -41,6 +41,7 @@ run_tests, ) from pants.core.subsystems.debug_adapter import DebugAdapterSubsystem +from pants.core.target_types import GenericTarget from pants.core.util_rules.distdir import DistDir from pants.core.util_rules.partitions import Partition, Partitions from pants.engine.addresses import Address @@ -754,6 +755,44 @@ def test_runtime_package_dependencies() -> None: assert snapshot.files == ("src.py/main.pex",) +def test_runtime_package_dependencies_via_generic_target() -> None: + """A `target()` listed in runtime_package_dependencies should be unwrapped so that its + packageable dependencies are built, rather than being silently ignored.""" + rule_runner = PythonRuleRunner( + rules=[ + build_runtime_package_dependencies, + *pex_from_targets.rules(), + *package_pex_binary.rules(), + *python_target_type_rules(), + QueryRule(BuiltPackageDependencies, [BuildPackageDependenciesRequest]), + ], + target_types=[PythonSourcesGeneratorTarget, PexBinary, GenericTarget], + ) + rule_runner.set_options(args=[], env_inherit={"PATH", "PYENV_ROOT", "HOME"}) + + rule_runner.write_files( + { + "src/py/main_a.py": "", + "src/py/main_b.py": "", + "src/py/BUILD": dedent( + """\ + pex_binary(name='bin_a', entry_point='main_a.py') + pex_binary(name='bin_b', entry_point='main_b.py') + target( + name='all_bins', + dependencies=[':bin_a', ':bin_b'], + ) + """ + ), + } + ) + input_field = RuntimePackageDependenciesField(["src/py:all_bins"], Address("fake")) + result = rule_runner.request( + BuiltPackageDependencies, [BuildPackageDependenciesRequest(input_field)] + ) + assert len(result) == 2 + + def test_timeout_calculation() -> None: def assert_timeout_calculated( *,