feat(pypi): add py.typed files to pyi_srcs (#4202)

Wheel libraries automatically include all their .pyi files in
`pyi_srcs`,
but py.typed files are just as important.  In this patch we begin
including them and make the same change to the runfiles py_library
as well.

(cherry picked from commit 28fe2e848379f09c65eb7aeb3c6569680ad6123d)

Work towards #4175
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d90b059..c51aef2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -99,6 +99,9 @@
 
 {#v2-4-0-added}
 ### Added
+* (pypi) `py.typed` files from wheels are now included in the `pyi_srcs` of the
+  wheel's `py_library`, alongside `.pyi` files. The `py.typed` file of
+  `//python/runfiles` is now included in its `pyi_srcs` as well.
 * (runfiles) Added {obj}`Runfiles.CreateOrRaise` to return a `Runfiles` instance
   or raise an error if runfiles cannot be found.
 * (uv) Added {obj}`directory` attribute to {obj}`lock` to support running `uv`
@@ -109,6 +112,7 @@
 
 
 
+
 {#v2-3-3}
 ## [2.3.3] - 2026-09-04
 
diff --git a/python/private/pypi/whl_library_srcs.bzl b/python/private/pypi/whl_library_srcs.bzl
index 128c46e..0556219 100644
--- a/python/private/pypi/whl_library_srcs.bzl
+++ b/python/private/pypi/whl_library_srcs.bzl
@@ -212,7 +212,7 @@
         )
 
         pyi_srcs = native.glob(
-            ["site-packages/**/*.pyi"],
+            ["site-packages/**/*.pyi", "site-packages/**/py.typed"],
             allow_empty = True,
         )
 
diff --git a/python/runfiles/BUILD.bazel b/python/runfiles/BUILD.bazel
index 0c9e74a..709bbd3 100644
--- a/python/runfiles/BUILD.bazel
+++ b/python/runfiles/BUILD.bazel
@@ -41,6 +41,7 @@
         # to the --experimental_python_import_all_repositories setting.
         "../..",
     ],
+    pyi_srcs = [":py_typed"],
     visibility = ["//visibility:public"],
 )
 
diff --git a/tests/pypi/whl_library_srcs/whl_library_srcs_tests.bzl b/tests/pypi/whl_library_srcs/whl_library_srcs_tests.bzl
index 1499170..fe4342b 100644
--- a/tests/pypi/whl_library_srcs/whl_library_srcs_tests.bzl
+++ b/tests/pypi/whl_library_srcs/whl_library_srcs_tests.bzl
@@ -145,6 +145,49 @@
 
 _tests.append(_test_sdist_excludes_record)
 
+def _test_pyi_srcs_includes_py_typed(env):
+    py_library_calls = []
+    m_glob = mocks.glob()
+    m_glob.results.append([])  # bin
+    m_glob.results.append([])  # rewrite-bin
+    m_glob.results.append([])  # rewrite-record
+    m_glob.results.append([])  # srcs
+    m_glob.results.append([])  # data
+    m_glob.results.append([
+        "site-packages/foo/__init__.pyi",
+        "site-packages/foo/py.typed",
+    ])  # pyi
+
+    whl_library_srcs(
+        name = "foo.whl",
+        filegroups = {},
+        native = struct(
+            filegroup = lambda **_: None,
+            config_setting = lambda **_: None,
+            glob = m_glob.glob,
+        ),
+        rules = struct(
+            py_library = lambda **kwargs: py_library_calls.append(kwargs),
+            create_inits = lambda **kwargs: [],
+            venv_rewrite_shebang = lambda **kwargs: None,
+            gen_wheel_record = lambda **kwargs: None,
+        ),
+    )
+
+    env.expect.that_collection(m_glob.calls).contains_at_least([
+        mocks.glob_call(
+            ["site-packages/**/*.pyi", "site-packages/**/py.typed"],
+            allow_empty = True,
+        ),
+    ])
+    env.expect.that_int(len(py_library_calls)).equals(1)
+    env.expect.that_collection(py_library_calls[0]["pyi_srcs"]).contains_exactly([
+        "site-packages/foo/__init__.pyi",
+        "site-packages/foo/py.typed",
+    ])
+
+_tests.append(_test_pyi_srcs_includes_py_typed)
+
 def _test_exclude_bazel_files(env):
     # Regression test: the `extracted_whl_files` glob must always exclude the
     # Bazel repo files, even when the wheel is not built from an sdist.