Repository: pantsbuild/pants

Issue:
export-codegen only exports Go, but not Python files
**Describe the bug**
I am trying out using python and go in the same repo. I setup a directory with proto definitions for gRPC service. Both python and Go can use these definitions in tests right now. However when I run `./pants export-codegen ::` it only exports Go files and not python. Go 1.18 and Python 3.9.

**Pants version**
2.11

**OS**
MacOS

**Additional info**
Sample `pants.toml`

```
[GLOBAL]
pants_version = "2.11.0"
backend_packages = [
  "pants.backend.python",
  "pants.backend.python.lint.black",
  "pants.backend.codegen.protobuf.python",
  "pants.backend.experimental.codegen.protobuf.go",
  "pants.backend.experimental.go",
]

[golang]
expected_version = "1.18"

[source]
root_patterns = [
  '/py',
  '/protos',
  '/pkg',
]
```

All python code is in `/py`, grpc in `/protos` and go in `/pkg`. 

`/protos/BUILD` contains: 
```
rotobuf_sources(
    name="protos",
    grpc=True,
)

python_requirements(
    name="requirements.txt",
)

python_requirement(name="grpc", requirements=["grpcio>=1.46.0"], modules=["grpc"])
```

`protos/requirements.txt` contains:
```
grpcio>=1.46.0
protobuf~=3.19.0
```

If I run `./pants test ::` both python and go tests are able to compile the rpc protobufs and actually test (I havent tried talking between go and py yet)


Discussion:
Running into this issue as well with Go + Python backends. The codegen backend that is registered last in `backend_packages` list is the one that ends up exporting files.

Resolving patch:
diff --git a/src/python/pants/backend/codegen/export_codegen_goal.py b/src/python/pants/backend/codegen/export_codegen_goal.py
--- a/src/python/pants/backend/codegen/export_codegen_goal.py
+++ b/src/python/pants/backend/codegen/export_codegen_goal.py
@@ -46,17 +46,16 @@ async def export_codegen(
     # We run all possible code generators. Running codegen requires specifying the expected
     # output_type, so we must inspect what is possible to generate.
     all_generate_request_types = union_membership.get(GenerateSourcesRequest)
-    inputs_to_outputs = {
-        req.input: req.output for req in all_generate_request_types if req.exportable
-    }
+    inputs_to_outputs = [
+        (req.input, req.output) for req in all_generate_request_types if req.exportable
+    ]
     codegen_sources_fields_with_output = []
     for tgt in targets:
         if not tgt.has_field(SourcesField):
             continue
         sources = tgt[SourcesField]
-        for input_type in inputs_to_outputs:
+        for input_type, output_type in inputs_to_outputs:
             if isinstance(sources, input_type):
-                output_type = inputs_to_outputs[input_type]
                 codegen_sources_fields_with_output.append((sources, output_type))
 
     if not codegen_sources_fields_with_output:
@@ -64,8 +63,8 @@ async def export_codegen(
             {
                 tgt_type.alias
                 for tgt_type in registered_target_types.types
-                for input_sources in inputs_to_outputs.keys()
-                if tgt_type.class_has_field(input_sources, union_membership=union_membership)
+                for input_source in {input_source for input_source, _ in inputs_to_outputs}
+                if tgt_type.class_has_field(input_source, union_membership=union_membership)
             }
         )
         logger.warning(


Test patch:
diff --git a/src/python/pants/backend/codegen/export_codegen_goal_test.py b/src/python/pants/backend/codegen/export_codegen_goal_test.py
--- a/src/python/pants/backend/codegen/export_codegen_goal_test.py
+++ b/src/python/pants/backend/codegen/export_codegen_goal_test.py
@@ -59,6 +59,11 @@ class GenNoExportRequest(GenerateSourcesRequest):
     exportable = False
 
 
+class Gen1DuplicatedRequest(GenerateSourcesRequest):
+    input = Gen1Sources
+    output = ResourceSourceField
+
+
 @rule
 async def gen1(_: Gen1Request) -> GeneratedSources:
     result = await Get(Snapshot, CreateDigest([FileContent("assets/README.md", b"Hello!")]))
@@ -76,6 +81,12 @@ async def gen_no_export(_: GenNoExportRequest) -> GeneratedSources:
     assert False, "Should not ever get here as `GenNoExportRequest.exportable==False`"
 
 
+@rule
+async def gen1_duplicated(_: Gen1DuplicatedRequest) -> GeneratedSources:
+    result = await Get(Snapshot, CreateDigest([FileContent("assets/DUPLICATED.md", b"Hello!")]))
+    return GeneratedSources(result)
+
+
 @pytest.fixture
 def rule_runner() -> RuleRunner:
     return RuleRunner(
@@ -84,9 +95,11 @@ def rule_runner() -> RuleRunner:
             gen1,
             gen2,
             gen_no_export,
+            gen1_duplicated,
             UnionRule(GenerateSourcesRequest, Gen1Request),
             UnionRule(GenerateSourcesRequest, Gen2Request),
             UnionRule(GenerateSourcesRequest, GenNoExportRequest),
+            UnionRule(GenerateSourcesRequest, Gen1DuplicatedRequest),
             *distdir.rules(),
         ],
         target_types=[Gen1Target, Gen2Target],
@@ -108,4 +121,5 @@ def test_export_codegen(rule_runner: RuleRunner) -> None:
     assert result.exit_code == 0
     parent_dir = Path(rule_runner.build_root, "dist", "codegen")
     assert (parent_dir / "assets" / "README.md").read_text() == "Hello!"
+    assert (parent_dir / "assets" / "DUPLICATED.md").read_text() == "Hello!"
     assert (parent_dir / "src" / "haskell" / "app.hs").read_text() == "10 * 4"