diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 7ac6a6bf1e4..6402c7f9cde 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -420,3 +420,4 @@ Created as part of the release process. + yjabri + Éric Jacob + Ólafur Páll Geirsson ++ Shubham Kumar Barnwal \ No newline at end of file diff --git a/src/python/pants/engine/target.py b/src/python/pants/engine/target.py index a310d226e88..d516b6b2070 100644 --- a/src/python/pants/engine/target.py +++ b/src/python/pants/engine/target.py @@ -569,6 +569,25 @@ def validate(self) -> None: context. If the validation only makes sense for certain goals acting on targets; those validations should be done in the associated rules. """ + sources_field_types = tuple( + ft for ft in self.field_types if issubclass(ft, SourcesField) + ) + if len(sources_field_types) > 1: + field_desc = ", ".join( + f"`{ft.alias}` ({ft.__name__})" + for ft in sorted(sources_field_types, key=lambda f: f.alias) + ) + raise InvalidFieldException( + softwrap( + f""" + The {self.alias} target at {self.address} has multiple fields that subclass + `SourcesField`, which is not supported: APIs such as `tgt.get(SourcesField)` + require at most one. Found: {field_desc}. + + Remove or consolidate the extra sources field(s). + """ + ) + ) def _validate_origin_sources_blocks(origin_sources_blocks: FrozenDict[str, SourceBlocks]) -> None: diff --git a/src/python/pants/engine/target_test.py b/src/python/pants/engine/target_test.py index 21796845c62..43f967a5279 100644 --- a/src/python/pants/engine/target_test.py +++ b/src/python/pants/engine/target_test.py @@ -471,6 +471,30 @@ def test_target_validate() -> None: FortranTarget({FortranVersion.alias: "bad"}, Address("", target_name="t")) +def test_target_rejects_multiple_sources_fields() -> None: + """Regression test for https://github.com/pantsbuild/pants/issues/17132.""" + + class PrimarySources(MultipleSourcesField): + alias = "sources" + default = ("*.ext",) + + class ExtraSources(MultipleSourcesField): + alias = "extra_sources" + + class InvalidMultiSourcesTarget(Target): + alias = "invalid_multi_sources" + core_fields = (FortranVersion, PrimarySources, ExtraSources) + + with pytest.raises(InvalidTargetException) as exc: + InvalidMultiSourcesTarget( + {FortranVersion.alias: "v1", PrimarySources.alias: ["a.ext"], ExtraSources.alias: ["b"]}, + Address("", target_name="t"), + ) + assert "multiple fields that subclass `SourcesField`" in str(exc.value) + assert "`extra_sources`" in str(exc.value) + assert "`sources`" in str(exc.value) + + def test_target_residence_dir() -> None: assert FortranTarget({}, Address("some_dir/subdir")).residence_dir == "some_dir/subdir" assert (