Repository: pantsbuild/pants

Issue:
Values from `__defaults__` are not treated the same as builtin field defaults.
> There is one downside with the current implementation for this, and that is that Pants will now treat the "default" glob for the `sources` field as "user provided", and as such err out for any globs that don't match any files.
>
> This is a pain if used in `__defaults__` to adjust the default sources glob for some target (say I want to have all `factories.py` files owned by `python_test_utils` rather than `python_sources` by default, but that file is not going to exist for all targets but [unmatched_build_file_globs](https://www.pantsbuild.org/v2.16/docs/reference-global#unmatched_build_file_globs) is a global option only..)

_Originally posted by @kaos in https://github.com/pantsbuild/pants/issues/17614#issuecomment-1548085565_
            
----

The context here is, that source globs provided as field values will err if they match _no_ files, where as source globs from the field default value does not. However, the values provided by `__defaults__` is treated the same as if they were provided directly on the target as a regular value.


Discussion:
Honestly I think we should just relax that error message, and not do anything special if a user-specified glob matches no files. That is, assume users are not dummies... That seems like by far the easiest solution, and I never liked that error.

After all, `__defaults__` are provided by users, and so sometimes an empty glob might indicate a real mistake they made there? Other times it wouldn't, as in your example. How do we decide when it's necessary to babysit the user's globs? I subit that we should never do this. An empty set is a set. 
Hmm.. interesting. Yea, maybe this is the only case where we treat a default value as being different from a user provided value. And could be a sign that we shouldn't do that. Agree this adds quite a bit of complexity to maintain, so I'm +1 to change this. (I guess even better would be if there was in the syntax so you could specify if you want all or any of the globs to match..)
Thanks for considering! I just overall think Pants is too intrusive and that we try to babysit the user in arbitrary and inconsistent ways, and probably shouldn't. The likelihood that a user specifies a glob at all is low, the likelihood that it unintentionally fails to match anything is even lower, and even when that happens, the user will figure it out and fix their glob?

I mean, "my glob doesn't match anything but it should" is just one of a variety of bad glob errors, and we don't (and can't) detect most of them (such as "my glob matches the wrong things"). And on the other hand there is a cost to all this code. 

So I say let's keep it simple. 

Resolving patch:
diff --git a/src/python/pants/engine/target.py b/src/python/pants/engine/target.py
--- a/src/python/pants/engine/target.py
+++ b/src/python/pants/engine/target.py
@@ -2190,23 +2190,19 @@ def path_globs(self, unmatched_build_file_globs: UnmatchedBuildFileGlobs) -> Pat
             [self.default] if self.default and isinstance(self.default, str) else self.default
         )
 
-        # Match any if we use default globs, else match all.
-        conjunction = (
-            GlobExpansionConjunction.all_match
-            if not default_globs or (set(self.globs) != set(default_globs))
-            else GlobExpansionConjunction.any_match
-        )
+        using_default_globs = default_globs and (set(self.globs) == set(default_globs)) or False
+
         # Use fields default error behavior if defined, if we use default globs else the provided
         # error behavior.
         error_behavior = (
             unmatched_build_file_globs.error_behavior
-            if conjunction == GlobExpansionConjunction.all_match
-            or self.default_glob_match_error_behavior is None
+            if not using_default_globs or self.default_glob_match_error_behavior is None
             else self.default_glob_match_error_behavior
         )
+
         return PathGlobs(
             (self._prefix_glob_with_address(glob) for glob in self.globs),
-            conjunction=conjunction,
+            conjunction=GlobExpansionConjunction.any_match,
             glob_match_error_behavior=error_behavior,
             description_of_origin=(
                 f"{self.address}'s `{self.alias}` field"


Test patch:
diff --git a/src/python/pants/engine/target_test.py b/src/python/pants/engine/target_test.py
--- a/src/python/pants/engine/target_test.py
+++ b/src/python/pants/engine/target_test.py
@@ -1061,7 +1061,7 @@ class GenSources(GenerateSourcesRequest):
                     "test/b",
                 ),
                 glob_match_error_behavior=GlobMatchErrorBehavior.warn,
-                conjunction=GlobExpansionConjunction.all_match,
+                conjunction=GlobExpansionConjunction.any_match,
                 description_of_origin="test:test's `sources` field",
             ),
             id="provided value warns on glob match error",
@@ -1108,7 +1108,7 @@ class TestMultipleSourcesField(MultipleSourcesField):
             expected_path_globs(
                 globs=("test/other_file",),
                 glob_match_error_behavior=GlobMatchErrorBehavior.warn,
-                conjunction=GlobExpansionConjunction.all_match,
+                conjunction=GlobExpansionConjunction.any_match,
                 description_of_origin="test:test's `source` field",
             ),
             id="provided value warns on glob match error",
@@ -1119,7 +1119,7 @@ class TestMultipleSourcesField(MultipleSourcesField):
             expected_path_globs(
                 globs=("test/life",),
                 glob_match_error_behavior=GlobMatchErrorBehavior.warn,
-                conjunction=GlobExpansionConjunction.all_match,
+                conjunction=GlobExpansionConjunction.any_match,
                 description_of_origin="test:test's `source` field",
             ),
             id="default glob conjunction",
diff --git a/tests/python/pants_test/integration/graph_integration_test.py b/tests/python/pants_test/integration/graph_integration_test.py
--- a/tests/python/pants_test/integration/graph_integration_test.py
+++ b/tests/python/pants_test/integration/graph_integration_test.py
@@ -15,7 +15,6 @@
 _SOURCES_TARGET_BASE = "testprojects/src/python/sources"
 
 _ERR_TARGETS = {
-    "testprojects/src/python/sources:some-missing-some-not": 'Unmatched glob from testprojects/src/python/sources:some-missing-some-not\'s `sources` field: "testprojects/src/python/sources/*.rs"',
     "testprojects/src/python/sources:missing-sources": 'Unmatched glob from testprojects/src/python/sources:missing-sources\'s `sources` field: "testprojects/src/python/sources/*.scala", excludes: ["testprojects/src/python/sources/*Spec.scala", "testprojects/src/python/sources/*Suite.scala", "testprojects/src/python/sources/*Test.scala"]',
 }