fix(bzlmod): convert coverage_tool Label to string (#4157)

The `coverage_tool` attribute on
`python.single_version_platform_override` is declared as a Label, but
the value was passed through unchanged to `python_repository`, which
declares `coverage_tool` as a string. This caused a type mismatch error
at module-extension evaluation time whenever `coverage_tool` was set.

Convert the Label to its canonical string form before storing it. Fixes
#2570.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
diff --git a/news/2570.fixed.md b/news/2570.fixed.md
new file mode 100644
index 0000000..f31115d
--- /dev/null
+++ b/news/2570.fixed.md
@@ -0,0 +1,3 @@
+(bzlmod) Fixed a type mismatch error when using {obj}`coverage_tool` with
+{obj}`python.single_version_platform_override`
+([#2570](https://github.com/bazel-contrib/rules_python/issues/2570)).
diff --git a/python/private/python.bzl b/python/private/python.bzl
index 70a3bd9..9fed939 100644
--- a/python/private/python.bzl
+++ b/python/private/python.bzl
@@ -626,7 +626,10 @@
         available_versions[tag.python_version] = {}
 
     if tag.coverage_tool:
-        available_versions[tag.python_version].setdefault("coverage_tool", {})[tag.platform] = tag.coverage_tool
+        # NOTE: tag.coverage_tool is a Label (so that it is resolved relative to the
+        # calling module), but downstream (python_repository.coverage_tool) it is
+        # consumed as a string, so convert it to its canonical string form here.
+        available_versions[tag.python_version].setdefault("coverage_tool", {})[tag.platform] = str(tag.coverage_tool)
     if tag.patch_strip:
         available_versions[tag.python_version].setdefault("patch_strip", {})[tag.platform] = tag.patch_strip
     if tag.patches:
diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl
index 5636b7e..ef7ccf0 100644
--- a/tests/python/python_tests.bzl
+++ b/tests/python/python_tests.bzl
@@ -481,7 +481,11 @@
                 ],
                 single_version_platform_override = [
                     python_ext.single_version_platform_override(
-                        coverage_tool = "specific_cov_tool",
+                        # `coverage_tool` is declared as `attr.label` on the tag class
+                        # (so bzlmod resolves it relative to the calling module), so it
+                        # is a `Label`, not a plain `str`, by the time it reaches here.
+                        # See https://github.com/bazel-contrib/rules_python/issues/2570.
+                        coverage_tool = Label("@my_module//:specific_cov_tool"),
                         patch_strip = 2,
                         patches = ["specific-patch.txt"],
                         platform = "aarch64-unknown-linux-gnu",
@@ -509,8 +513,15 @@
         "strip_prefix": {"aarch64-unknown-linux-gnu": "prefix"},
         "url": {"aarch64-unknown-linux-gnu": ["example.org"]},
     })
+
+    # The Label must be converted to its canonical string form: `python_repository`
+    # (which ultimately consumes this value) declares `coverage_tool` as `attr.string`.
+    coverage_tool = py.config.default["tool_versions"]["3.13.99"]["coverage_tool"]["aarch64-unknown-linux-gnu"]
+    env.expect.that_str(type(coverage_tool)).equals("string")
+    env.expect.that_str(coverage_tool).equals(str(Label("@my_module//:specific_cov_tool")))
+
     env.expect.that_dict(py.config.default["tool_versions"]["3.13.99"]).contains_exactly({
-        "coverage_tool": {"aarch64-unknown-linux-gnu": "specific_cov_tool"},
+        "coverage_tool": {"aarch64-unknown-linux-gnu": coverage_tool},
         "patch_strip": {"aarch64-unknown-linux-gnu": 2},
         "patches": {"aarch64-unknown-linux-gnu": ["specific-patch.txt"]},
         "sha256": {"aarch64-unknown-linux-gnu": "deadb00f"},