Repository: pantsbuild/pants

Issue:
Wrong backend in generated subsystem documentation
See https://www.pantsbuild.org/docs/reference-python-infer which states that the subsystem is provided by the `pants.backend.awslambda.python` when in reality it is provided by `pants.backend.python`.


Discussion:
Confirmed.

The trouble here is correctly identifying the true source of registered rules, as the python rules are registered also by the awslambda backend, here: 

https://github.com/pantsbuild/pants/blob/fa7cdebca43c217cdc6e7952a2c0c956e8617fe3/src/python/pants/backend/awslambda/python/register.py#L15-L16

And we do have a list of _all_ backends registering each rule, but rather than present them all, picked "the first one" for a lack of indicator of which one to use (as it were). Now I realize that it may be **better to pick the _shortest_ backend** available, as that avoids picking a nested backend, for instance, as in this case.

And the reason it picks the wrong backend for the docs site is due to the awslambda backend being registered before the python one, here:

https://github.com/pantsbuild/pants/blob/fa7cdebca43c217cdc6e7952a2c0c956e8617fe3/build-support/bin/generate_docs.py#L202-L206


Suggested course of action is to adjust this to pick the shortest available backend for each subsystem/rule rather than the first, to get the most "top level" backend for each case.
I think that is a sensible heuristic. 

We know the package the subsytem is defined in, no? Can we use that in some way?
yeah i think it'd be even better if we can correlate the file the rule/subsystem comes from to which backend is the nearest distance

```
>>> defaultdict.__module__
'collections'
```
Good ideas, will look into that.

Actually, I think it complicates things to look at the `__module__` of the types. Simply picking the shortest available provider is likely the one we want for any case. Example:
```
DIFF LIB SUGGEST pants.backend.experimental.scala.debug_goals \
  from ('pants.backend.experimental.scala', 'pants.backend.experimental.scala.lint.scalafmt', 'pants.backend.experimental.scala.debug_goals') \
  for pants.backend.scala.target_types
```

Here I've used `difflib.get_close_matches` to pick out a candidate provider, and I think it got the wrong one. Would expect we want `pants.backend.experimental.scala` here, which also is the shortest. Looking at a bunch of example like this, it often gets it right, but not always, and I have yet to find an example where the shortest one is not suitable.

I made an attempt at pruning sub modules from the list of providers to get rid of the false matches found by the difflib, and while I think that would work, it feels overly complex to simply just sticking with the shortest one to begin with.


Resolving patch:
diff --git a/src/python/pants/help/help_info_extracter.py b/src/python/pants/help/help_info_extracter.py
--- a/src/python/pants/help/help_info_extracter.py
+++ b/src/python/pants/help/help_info_extracter.py
@@ -322,7 +322,7 @@ def load() -> OptionScopeHelpInfo:
                     )
                 provider = ""
                 if subsystem_cls is not None and build_configuration is not None:
-                    provider = cls.get_first_provider(
+                    provider = cls.get_provider(
                         build_configuration.subsystem_to_providers.get(subsystem_cls)
                     )
                 return HelpInfoExtracter(scope_info.scope).get_option_scope_help_info(
@@ -340,7 +340,7 @@ def load() -> GoalHelpInfo:
                 subsystem_cls = scope_info.subsystem_cls
                 assert subsystem_cls is not None
                 if build_configuration is not None:
-                    provider = cls.get_first_provider(
+                    provider = cls.get_provider(
                         build_configuration.subsystem_to_providers.get(subsystem_cls)
                     )
                 goal_subsystem_cls = cast(Type[GoalSubsystem], subsystem_cls)
@@ -359,12 +359,12 @@ def load() -> TargetTypeHelpInfo:
                 return TargetTypeHelpInfo.create(
                     target_type,
                     union_membership=union_membership,
-                    provider=cls.get_first_provider(
+                    provider=cls.get_provider(
                         build_configuration
                         and build_configuration.target_type_to_providers.get(target_type)
                         or None
                     ),
-                    get_field_type_provider=lambda field_type: cls.get_first_provider(
+                    get_field_type_provider=lambda field_type: cls.get_provider(
                         build_configuration.union_rule_to_providers.get(
                             UnionRule(target_type._plugin_field_cls, field_type)
                         )
@@ -478,10 +478,11 @@ def compute_choices(kwargs) -> tuple[str, ...] | None:
             return None
 
     @staticmethod
-    def get_first_provider(providers: tuple[str, ...] | None) -> str:
+    def get_provider(providers: tuple[str, ...] | None) -> str:
         if not providers:
             return ""
-        return providers[0]
+        # Pick the shortest backend name.
+        return sorted(providers, key=len)[0]
 
     @staticmethod
     def maybe_cleandoc(doc: str | None) -> str | None:
@@ -503,7 +504,7 @@ def get_rule_infos(
                 name=rule.canonical_name,
                 description=rule.desc,
                 help=cls.maybe_cleandoc(rule.func.__doc__),
-                provider=cls.get_first_provider(providers),
+                provider=cls.get_provider(providers),
                 input_types=tuple(selector.__name__ for selector in rule.input_selectors),
                 input_gets=tuple(str(constraints) for constraints in rule.input_gets),
                 output_type=rule.output_type.__name__,


Test patch: