Repository: pantsbuild/pants

Issue:
Expose field default values in a BUILD file
**Is your feature request related to a problem? Please describe.**
When I want to add a source to a `python_sources` field, I have to repeat the default globs (which includes remembering to omit tests and include `.pyi` files). This is painful and not upgrade-friendly.

Additionally, people want to do `python_sources(sources=["**/*.py"])` however, that isn't quite right (again, tests and pyis, and conftests).

**Describe the solution you'd like**
A way to refer to the defaults inside my BUILD file so I can mangle it.

Strawman:

```python
# Using `python_sources.sources.default`
python_sources(
    sources = python_sources.sources.default + ["TotallyAPythonFile"]
)
# or
python_sources(
    sources = [
        "**/" + default for default in
        python_sources.sources.default   
    ]
)
```

**Describe alternatives you've considered**
Hardcording :vomiting_face: 

**Additional context**
:tophat: 



Discussion:

Resolving patch:
diff --git a/src/python/pants/engine/internals/parser.py b/src/python/pants/engine/internals/parser.py
--- a/src/python/pants/engine/internals/parser.py
+++ b/src/python/pants/engine/internals/parser.py
@@ -10,14 +10,17 @@
 from difflib import get_close_matches
 from io import StringIO
 from pathlib import PurePath
-from typing import Any, Iterable
+from typing import Any
 
+from pants.base.deprecated import warn_or_error
 from pants.base.exceptions import MappingError
 from pants.base.parse_context import ParseContext
 from pants.build_graph.build_file_aliases import BuildFileAliases
 from pants.engine.internals.defaults import BuildFileDefaultsParserState, SetDefaultsT
 from pants.engine.internals.dep_rules import BuildFileDependencyRulesParserState
 from pants.engine.internals.target_adaptor import TargetAdaptor
+from pants.engine.target import Field, ImmutableValue, RegisteredTargetTypes
+from pants.engine.unions import UnionMembership
 from pants.util.docutil import doc_url
 from pants.util.frozendict import FrozenDict
 from pants.util.strutil import softwrap
@@ -94,25 +97,41 @@ def set_dependencies_rules(self, *args, **kwargs) -> None:
             self._dependencies_rules.set_dependency_rules(self.filepath(), *args, **kwargs)
 
 
+class RegistrarField:
+    __slots__ = ("_field_type",)
+
+    def __init__(self, field_type: type[Field]) -> None:
+        self._field_type = field_type
+
+    @property
+    def default(self) -> ImmutableValue:
+        return self._field_type.default
+
+
 class Parser:
     def __init__(
         self,
         *,
         build_root: str,
-        target_type_aliases: Iterable[str],
+        registered_target_types: RegisteredTargetTypes,
+        union_membership: UnionMembership,
         object_aliases: BuildFileAliases,
         ignore_unrecognized_symbols: bool,
     ) -> None:
         self._symbols, self._parse_state = self._generate_symbols(
-            build_root, target_type_aliases, object_aliases
+            build_root,
+            object_aliases,
+            registered_target_types,
+            union_membership,
         )
         self.ignore_unrecognized_symbols = ignore_unrecognized_symbols
 
     @staticmethod
     def _generate_symbols(
         build_root: str,
-        target_type_aliases: Iterable[str],
         object_aliases: BuildFileAliases,
+        registered_target_types: RegisteredTargetTypes,
+        union_membership: UnionMembership,
     ) -> tuple[FrozenDict[str, Any], ParseState]:
         # N.B.: We re-use the thread local ParseState across symbols for performance reasons.
         # This allows a single construction of all symbols here that can be re-used for each BUILD
@@ -122,6 +141,27 @@ def _generate_symbols(
         class Registrar:
             def __init__(self, type_alias: str) -> None:
                 self._type_alias = type_alias
+                for field_type in registered_target_types.aliases_to_types[
+                    type_alias
+                ].class_field_types(union_membership):
+                    registrar_field = RegistrarField(field_type)
+                    setattr(self, field_type.alias, registrar_field)
+                    if field_type.deprecated_alias:
+
+                        def deprecated_field(self):
+                            # TODO(17720) Support fixing automatically with `build-file` deprecation
+                            # fixer.
+                            warn_or_error(
+                                removal_version=field_type.deprecated_alias_removal_version,
+                                entity=f"the field name {field_type.deprecated_alias}",
+                                hint=(
+                                    f"Instead, use `{type_alias}.{field_type.alias}`, which "
+                                    "behaves the same."
+                                ),
+                            )
+                            return registrar_field
+
+                        setattr(self, field_type.deprecated_alias, property(deprecated_field))
 
             def __str__(self) -> str:
                 """The BuildFileDefaultsParserState.set_defaults() rely on string inputs.
@@ -154,7 +194,7 @@ def __call__(self, **kwargs: Any) -> TargetAdaptor:
             "__dependents_rules__": parse_state.set_dependents_rules,
             "__dependencies_rules__": parse_state.set_dependencies_rules,
         }
-        symbols.update((alias, Registrar(alias)) for alias in target_type_aliases)
+        symbols.update((alias, Registrar(alias)) for alias in registered_target_types.aliases)
 
         parse_context = ParseContext(
             build_root=build_root, type_aliases=symbols, filepath_oracle=parse_state
diff --git a/src/python/pants/init/engine_initializer.py b/src/python/pants/init/engine_initializer.py
--- a/src/python/pants/init/engine_initializer.py
+++ b/src/python/pants/init/engine_initializer.py
@@ -242,7 +242,8 @@ def setup_graph_extended(
         def parser_singleton() -> Parser:
             return Parser(
                 build_root=build_root_path,
-                target_type_aliases=registered_target_types.aliases,
+                registered_target_types=registered_target_types,
+                union_membership=union_membership,
                 object_aliases=build_configuration.registered_aliases,
                 ignore_unrecognized_symbols=ignore_unrecognized_build_file_symbols,
             )


Test patch:
diff --git a/src/python/pants/engine/internals/build_files_test.py b/src/python/pants/engine/internals/build_files_test.py
--- a/src/python/pants/engine/internals/build_files_test.py
+++ b/src/python/pants/engine/internals/build_files_test.py
@@ -11,6 +11,7 @@
 
 from pants.build_graph.address import BuildFileAddressRequest, MaybeAddress, ResolveError
 from pants.build_graph.build_file_aliases import BuildFileAliases
+from pants.core.target_types import GenericTarget
 from pants.engine.addresses import Address, AddressInput, BuildFileAddress
 from pants.engine.fs import DigestContents, FileContent, PathGlobs
 from pants.engine.internals.build_files import (
@@ -56,7 +57,8 @@ def test_parse_address_family_empty() -> None:
         rule_args=[
             Parser(
                 build_root="",
-                target_type_aliases=[],
+                registered_target_types=RegisteredTargetTypes({}),
+                union_membership=UnionMembership({}),
                 object_aliases=BuildFileAliases(),
                 ignore_unrecognized_symbols=False,
             ),
@@ -99,7 +101,8 @@ def run_prelude_parsing_rule(prelude_content: str) -> BuildFilePreludeSymbols:
             BuildFileOptions((), prelude_globs=("prelude",)),
             Parser(
                 build_root="",
-                target_type_aliases=["target"],
+                registered_target_types=RegisteredTargetTypes({"target": GenericTarget}),
+                union_membership=UnionMembership({}),
                 object_aliases=BuildFileAliases(),
                 ignore_unrecognized_symbols=False,
             ),
@@ -180,7 +183,7 @@ class MockDepsField(Dependencies):
 
 
 class MockMultipleSourcesField(MultipleSourcesField):
-    pass
+    default = ("*.mock",)
 
 
 class MockTgt(Target):
@@ -376,6 +379,28 @@ def test_parametrize_defaults(target_adaptor_rule_runner: RuleRunner) -> None:
     assert target_adaptor.kwargs["tags"] == ParametrizeDefault(a=("a", "root"), b=("non-root", "b"))
 
 
+def test_augment_target_field_defaults(target_adaptor_rule_runner: RuleRunner) -> None:
+    target_adaptor_rule_runner.write_files(
+        {
+            "BUILD": dedent(
+                """
+                mock_tgt(
+                  sources=(
+                    "*.added",
+                    *mock_tgt.sources.default,
+                  ),
+                )
+                """
+            ),
+        },
+    )
+    target_adaptor = target_adaptor_rule_runner.request(
+        TargetAdaptor,
+        [TargetAdaptorRequest(Address(""), description_of_origin="tests")],
+    )
+    assert target_adaptor.kwargs["sources"] == ("*.added", "*.mock")
+
+
 def test_target_adaptor_not_found(target_adaptor_rule_runner: RuleRunner) -> None:
     with pytest.raises(ExecutionError) as exc:
         target_adaptor_rule_runner.request(
@@ -428,7 +453,8 @@ def test_build_files_share_globals() -> None:
             BuildFileOptions((), prelude_globs=("prelude",)),
             Parser(
                 build_root="",
-                target_type_aliases=[],
+                registered_target_types=RegisteredTargetTypes({}),
+                union_membership=UnionMembership({}),
                 object_aliases=BuildFileAliases(),
                 ignore_unrecognized_symbols=False,
             ),
diff --git a/src/python/pants/engine/internals/mapper_test.py b/src/python/pants/engine/internals/mapper_test.py
--- a/src/python/pants/engine/internals/mapper_test.py
+++ b/src/python/pants/engine/internals/mapper_test.py
@@ -11,6 +11,7 @@
 from pants.base.exceptions import MappingError
 from pants.build_graph.address import Address
 from pants.build_graph.build_file_aliases import BuildFileAliases
+from pants.core.target_types import GenericTarget
 from pants.engine.internals.defaults import BuildFileDefaults, BuildFileDefaultsParserState
 from pants.engine.internals.mapper import (
     AddressFamily,
@@ -31,7 +32,8 @@ def parse_address_map(build_file: str, *, ignore_unrecognized_symbols: bool = Fa
     path = "/dev/null"
     parser = Parser(
         build_root="",
-        target_type_aliases=["thing"],
+        registered_target_types=RegisteredTargetTypes({"thing": GenericTarget}),
+        union_membership=UnionMembership({}),
         object_aliases=BuildFileAliases(),
         ignore_unrecognized_symbols=ignore_unrecognized_symbols,
     )
diff --git a/src/python/pants/engine/internals/parser_test.py b/src/python/pants/engine/internals/parser_test.py
--- a/src/python/pants/engine/internals/parser_test.py
+++ b/src/python/pants/engine/internals/parser_test.py
@@ -6,6 +6,7 @@
 import pytest
 
 from pants.build_graph.build_file_aliases import BuildFileAliases
+from pants.core.target_types import GenericTarget
 from pants.engine.internals.defaults import BuildFileDefaults, BuildFileDefaultsParserState
 from pants.engine.internals.parser import (
     BuildFilePreludeSymbols,
@@ -30,7 +31,8 @@ def defaults_parser_state() -> BuildFileDefaultsParserState:
 def test_imports_banned(defaults_parser_state: BuildFileDefaultsParserState) -> None:
     parser = Parser(
         build_root="",
-        target_type_aliases=[],
+        registered_target_types=RegisteredTargetTypes({}),
+        union_membership=UnionMembership({}),
         object_aliases=BuildFileAliases(),
         ignore_unrecognized_symbols=False,
     )
@@ -55,7 +57,10 @@ def test_unrecognized_symbol(defaults_parser_state: BuildFileDefaultsParserState
     def perform_test(extra_targets: list[str], dym: str) -> None:
         parser = Parser(
             build_root="",
-            target_type_aliases=["tgt", *extra_targets],
+            registered_target_types=RegisteredTargetTypes(
+                {alias: GenericTarget for alias in ("tgt", *extra_targets)}
+            ),
+            union_membership=UnionMembership({}),
             object_aliases=build_file_aliases,
             ignore_unrecognized_symbols=False,
         )
@@ -83,7 +88,10 @@ def perform_test(extra_targets: list[str], dym: str) -> None:
         with no_exception():
             parser = Parser(
                 build_root="",
-                target_type_aliases=["tgt", *extra_targets],
+                registered_target_types=RegisteredTargetTypes(
+                    {alias: GenericTarget for alias in ("tgt", *extra_targets)}
+                ),
+                union_membership=UnionMembership({}),
                 object_aliases=build_file_aliases,
                 ignore_unrecognized_symbols=True,
             )