feat: add pyi attributes/fields, original source fields (#2538)

This adds attributes and fields of use to static analysis.

For type definition files (usually `.pyi` files), the `pyi_srcs` and
`pyi_deps`
fields are added to the rules. They end up in the PyInfo fields
direct_pyi_files
and transitive_pyi_files.

So that static analysis tools can retain access to a target's Python
source files,
even if precompiling is enabled, `direct_original_sources` and
`transitive_original_sources` fields are added to PyInfo.

Work towards https://github.com/bazelbuild/rules_python/issues/2537,
https://github.com/bazelbuild/rules_python/issues/296
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fd5d455..7c2f84f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -97,6 +97,13 @@
     * 3.11.11
     * 3.12.8
     * 3.13.1
+* (rules) Attributes for type definition files (`.pyi` files) and type-checking
+  only dependencies added. See {obj}`py_library.pyi_srcs` and
+  `py_library.pyi_deps` (and the same named attributes for `py_binary` and
+  `py_test`).
+* (providers) {obj}`PyInfo` has new fields to aid static analysis tools:
+  {obj}`direct_original_sources`, {obj}`direct_pyi_files`,
+  {obj}`transitive_original_sources`, {obj}`transitive_pyi_files`.
 
 [20241206]: https://github.com/astral-sh/python-build-standalone/releases/tag/20241206
 
diff --git a/python/private/attributes.bzl b/python/private/attributes.bzl
index e62abf9..dfe0d4e 100644
--- a/python/private/attributes.bzl
+++ b/python/private/attributes.bzl
@@ -375,6 +375,35 @@
 * `omit_source`: Don't include the original py source.
 """,
         ),
+        "pyi_deps": attr.label_list(
+            doc = """
+Dependencies providing type definitions the library needs.
+
+These are dependencies that satisfy imports guarded by `typing.TYPE_CHECKING`.
+These are build-time only dependencies and not included as part of a runnable
+program (packaging rules may include them, however).
+
+:::{versionadded} VERSION_NEXT_FEATURE
+:::
+""",
+            providers = [
+                [PyInfo],
+                [CcInfo],
+            ] + _MaybeBuiltinPyInfo,
+        ),
+        "pyi_srcs": attr.label_list(
+            doc = """
+Type definition files for the library.
+
+These are typically `.pyi` files, but other file types for type-checker specific
+formats are allowed. These files are build-time only dependencies and not included
+as part of a runnable program (packaging rules may include them, however).
+
+:::{versionadded} VERSION_NEXT_FEATURE
+:::
+""",
+            allow_files = True,
+        ),
         # Required attribute, but details vary by rule.
         # Use create_srcs_attr to create one.
         "srcs": None,
diff --git a/python/private/common.bzl b/python/private/common.bzl
index 9c285f9..b6a5453 100644
--- a/python/private/common.bzl
+++ b/python/private/common.bzl
@@ -408,6 +408,7 @@
 def create_py_info(
         ctx,
         *,
+        original_sources,
         required_py_files,
         required_pyc_files,
         implicit_pyc_files,
@@ -417,6 +418,7 @@
 
     Args:
         ctx: rule ctx.
+        original_sources: `depset[File]`; the original input sources from `srcs`
         required_py_files: `depset[File]`; the direct, `.py` sources for the
             target that **must** be included by downstream targets. This should
             only be Python source files. It should not include pyc files.
@@ -435,10 +437,13 @@
         transitive sources collected from dependencies (the latter is only
         necessary for deprecated extra actions support).
     """
-
     py_info = PyInfoBuilder()
+    py_info.direct_original_sources.add(original_sources)
     py_info.direct_pyc_files.add(required_pyc_files)
+    py_info.direct_pyi_files.add(ctx.files.pyi_srcs)
+    py_info.transitive_original_sources.add(original_sources)
     py_info.transitive_pyc_files.add(required_pyc_files)
+    py_info.transitive_pyi_files.add(ctx.files.pyi_srcs)
     py_info.transitive_implicit_pyc_files.add(implicit_pyc_files)
     py_info.transitive_implicit_pyc_source_files.add(implicit_pyc_source_files)
     py_info.imports.add(imports)
@@ -457,6 +462,10 @@
                 if f.extension == "py":
                     py_info.transitive_sources.add(f)
                 py_info.merge_uses_shared_libraries(cc_helper.is_valid_shared_library_artifact(f))
+    for target in ctx.attr.pyi_deps:
+        # PyInfo may not be present e.g. cc_library rules.
+        if PyInfo in target or (BuiltinPyInfo != None and BuiltinPyInfo in target):
+            py_info.merge(_get_py_info(target))
 
     deps_transitive_sources = py_info.transitive_sources.build()
     py_info.transitive_sources.add(required_py_files)
diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl
index 40c7410..20af98d 100644
--- a/python/private/py_executable.bzl
+++ b/python/private/py_executable.bzl
@@ -985,6 +985,7 @@
         runfiles_details = runfiles_details,
         main_py = main_py,
         imports = imports,
+        original_sources = direct_sources,
         required_py_files = required_py_files,
         required_pyc_files = required_pyc_files,
         implicit_pyc_files = implicit_pyc_files,
@@ -1548,6 +1549,7 @@
         ctx,
         executable,
         main_py,
+        original_sources,
         required_py_files,
         required_pyc_files,
         implicit_pyc_files,
@@ -1566,6 +1568,8 @@
         ctx: The rule ctx.
         executable: File; the target's executable file.
         main_py: File; the main .py entry point.
+        original_sources: `depset[File]` the direct `.py` sources for the
+            target that were the original input sources.
         required_py_files: `depset[File]` the direct, `.py` sources for the
             target that **must** be included by downstream targets. This should
             only be Python source files. It should not include pyc files.
@@ -1649,6 +1653,7 @@
 
     py_info, deps_transitive_sources, builtin_py_info = create_py_info(
         ctx,
+        original_sources = original_sources,
         required_py_files = required_py_files,
         required_pyc_files = required_pyc_files,
         implicit_pyc_files = implicit_pyc_files,
diff --git a/python/private/py_info.bzl b/python/private/py_info.bzl
index 4b2b888..2a02685 100644
--- a/python/private/py_info.bzl
+++ b/python/private/py_info.bzl
@@ -38,7 +38,11 @@
         direct_pyc_files = depset(),
         transitive_pyc_files = depset(),
         transitive_implicit_pyc_files = depset(),
-        transitive_implicit_pyc_source_files = depset()):
+        transitive_implicit_pyc_source_files = depset(),
+        direct_original_sources = depset(),
+        transitive_original_sources = depset(),
+        direct_pyi_files = depset(),
+        transitive_pyi_files = depset()):
     _check_arg_type("transitive_sources", "depset", transitive_sources)
 
     # Verify it's postorder compatible, but retain is original ordering.
@@ -53,14 +57,24 @@
 
     _check_arg_type("transitive_implicit_pyc_files", "depset", transitive_pyc_files)
     _check_arg_type("transitive_implicit_pyc_source_files", "depset", transitive_pyc_files)
+
+    _check_arg_type("direct_original_sources", "depset", direct_original_sources)
+    _check_arg_type("transitive_original_sources", "depset", transitive_original_sources)
+
+    _check_arg_type("direct_pyi_files", "depset", direct_pyi_files)
+    _check_arg_type("transitive_pyi_files", "depset", transitive_pyi_files)
     return {
+        "direct_original_sources": direct_original_sources,
         "direct_pyc_files": direct_pyc_files,
+        "direct_pyi_files": direct_pyi_files,
         "has_py2_only_sources": has_py2_only_sources,
         "has_py3_only_sources": has_py2_only_sources,
         "imports": imports,
         "transitive_implicit_pyc_files": transitive_implicit_pyc_files,
         "transitive_implicit_pyc_source_files": transitive_implicit_pyc_source_files,
+        "transitive_original_sources": transitive_original_sources,
         "transitive_pyc_files": transitive_pyc_files,
+        "transitive_pyi_files": transitive_pyi_files,
         "transitive_sources": transitive_sources,
         "uses_shared_libraries": uses_shared_libraries,
     }
@@ -69,6 +83,18 @@
     doc = "Encapsulates information provided by the Python rules.",
     init = _PyInfo_init,
     fields = {
+        "direct_original_sources": """
+:type: depset[File]
+
+The `.py` source files (if any) that are considered directly provided by
+the target. This field is intended so that static analysis tools can recover the
+original Python source files, regardless of any build settings (e.g.
+precompiling), so they can analyze source code. The values are typically the
+`.py` files in the `srcs` attribute (or equivalent).
+
+::::{versionadded} 1.1.0
+::::
+""",
         "direct_pyc_files": """
 :type: depset[File]
 
@@ -79,6 +105,21 @@
 to forcibly enable precompiling for itself. Downstream binaries are expected
 to always include these files, as the originating target expects them to exist.
 """,
+        "direct_pyi_files": """
+:type: depset[File]
+
+Type definition files (usually `.pyi` files) for the Python modules provided by
+this target. Usually they describe the source files listed in
+`direct_original_sources`. This field is primarily for static analysis tools.
+
+:::{note}
+This may contain implementation-specific file types specific to a particular
+type checker.
+:::
+
+::::{versionadded} 1.1.0
+::::
+""",
         "has_py2_only_sources": """
 :type: bool
 
@@ -117,6 +158,21 @@
 ::::{versionadded} 0.37.0
 ::::
 """,
+        "transitive_original_sources": """
+:type: depset[File]
+
+The transitive set of `.py` source files (if any) that are considered the
+original sources for this target and its transitive dependencies. This field is
+intended so that static analysis tools can recover the original Python source
+files, regardless of any build settings (e.g. precompiling), so they can analyze
+source code. The values are typically the `.py` files in the `srcs` attribute
+(or equivalent).
+
+This is superset of `direct_original_sources`.
+
+::::{versionadded} 1.1.0
+::::
+""",
         "transitive_pyc_files": """
 :type: depset[File]
 
@@ -126,6 +182,22 @@
 to forcibly enable precompiling for itself. Downstream binaries are expected
 to always include these files, as the originating target expects them to exist.
 """,
+        "transitive_pyi_files": """
+:type: depset[File]
+
+The transitive set of type definition files (usually `.pyi` files) for the
+Python modules for this target and its transitive dependencies. this target.
+Usually they describe the source files listed in `transitive_original_sources`.
+This field is primarily for static analysis tools.
+
+:::{note}
+This may contain implementation-specific file types specific to a particular
+type checker.
+:::
+
+::::{versionadded} 1.1.0
+::::
+""",
         "transitive_sources": """\
 :type: depset[File]
 
@@ -165,7 +237,9 @@
         _uses_shared_libraries = [False],
         build = lambda *a, **k: _PyInfoBuilder_build(self, *a, **k),
         build_builtin_py_info = lambda *a, **k: _PyInfoBuilder_build_builtin_py_info(self, *a, **k),
+        direct_original_sources = builders.DepsetBuilder(),
         direct_pyc_files = builders.DepsetBuilder(),
+        direct_pyi_files = builders.DepsetBuilder(),
         get_has_py2_only_sources = lambda *a, **k: _PyInfoBuilder_get_has_py2_only_sources(self, *a, **k),
         get_has_py3_only_sources = lambda *a, **k: _PyInfoBuilder_get_has_py3_only_sources(self, *a, **k),
         get_uses_shared_libraries = lambda *a, **k: _PyInfoBuilder_get_uses_shared_libraries(self, *a, **k),
@@ -182,7 +256,9 @@
         set_uses_shared_libraries = lambda *a, **k: _PyInfoBuilder_set_uses_shared_libraries(self, *a, **k),
         transitive_implicit_pyc_files = builders.DepsetBuilder(),
         transitive_implicit_pyc_source_files = builders.DepsetBuilder(),
+        transitive_original_sources = builders.DepsetBuilder(),
         transitive_pyc_files = builders.DepsetBuilder(),
+        transitive_pyi_files = builders.DepsetBuilder(),
         transitive_sources = builders.DepsetBuilder(),
     )
     return self
@@ -221,13 +297,39 @@
     return self
 
 def _PyInfoBuilder_merge(self, *infos, direct = []):
+    """Merge other PyInfos into this PyInfo.
+
+    Args:
+        self: implicitly added.
+        *infos: {type}`PyInfo` objects to merge in, but only merge in their
+            information into this object's transitive fields.
+        direct: {type}`list[PyInfo]` objects to merge in, but also merge their
+            direct fields into this object's direct fields.
+
+    Returns:
+        {type}`PyInfoBuilder` the current object
+    """
     return self.merge_all(list(infos), direct = direct)
 
 def _PyInfoBuilder_merge_all(self, transitive, *, direct = []):
+    """Merge other PyInfos into this PyInfo.
+
+    Args:
+        self: implicitly added.
+        transitive: {type}`list[PyInfo]` objects to merge in, but only merge in
+            their information into this object's transitive fields.
+        direct: {type}`list[PyInfo]` objects to merge in, but also merge their
+            direct fields into this object's direct fields.
+
+    Returns:
+        {type}`PyInfoBuilder` the current object
+    """
     for info in direct:
         # BuiltinPyInfo doesn't have this field
         if hasattr(info, "direct_pyc_files"):
+            self.direct_original_sources.add(info.direct_original_sources)
             self.direct_pyc_files.add(info.direct_pyc_files)
+            self.direct_pyi_files.add(info.direct_pyi_files)
 
     for info in direct + transitive:
         self.imports.add(info.imports)
@@ -240,11 +342,24 @@
         if hasattr(info, "transitive_pyc_files"):
             self.transitive_implicit_pyc_files.add(info.transitive_implicit_pyc_files)
             self.transitive_implicit_pyc_source_files.add(info.transitive_implicit_pyc_source_files)
+            self.transitive_original_sources.add(info.transitive_original_sources)
             self.transitive_pyc_files.add(info.transitive_pyc_files)
+            self.transitive_pyi_files.add(info.transitive_pyi_files)
 
     return self
 
 def _PyInfoBuilder_merge_target(self, target):
+    """Merge a target's Python information in this object.
+
+    Args:
+        self: implicitly added.
+        target: {type}`Target` targets that provide PyInfo, or other relevant
+        providers, will be merged into this object. If a target doesn't provide
+        any relevant providers, it is ignored.
+
+    Returns:
+        {type}`PyInfoBuilder` the current object.
+    """
     if PyInfo in target:
         self.merge(target[PyInfo])
     elif BuiltinPyInfo != None and BuiltinPyInfo in target:
@@ -252,6 +367,18 @@
     return self
 
 def _PyInfoBuilder_merge_targets(self, targets):
+    """Merge multiple targets into this object.
+
+    Args:
+        self: implicitly added.
+        targets: {type}`list[Target]`
+        targets that provide PyInfo, or other relevant
+        providers, will be merged into this object. If a target doesn't provide
+        any relevant providers, it is ignored.
+
+    Returns:
+        {type}`PyInfoBuilder` the current object.
+    """
     for t in targets:
         self.merge_target(t)
     return self
@@ -259,10 +386,14 @@
 def _PyInfoBuilder_build(self):
     if config.enable_pystar:
         kwargs = dict(
+            direct_original_sources = self.direct_original_sources.build(),
             direct_pyc_files = self.direct_pyc_files.build(),
-            transitive_pyc_files = self.transitive_pyc_files.build(),
+            direct_pyi_files = self.direct_pyi_files.build(),
             transitive_implicit_pyc_files = self.transitive_implicit_pyc_files.build(),
             transitive_implicit_pyc_source_files = self.transitive_implicit_pyc_source_files.build(),
+            transitive_original_sources = self.transitive_original_sources.build(),
+            transitive_pyc_files = self.transitive_pyc_files.build(),
+            transitive_pyi_files = self.transitive_pyi_files.build(),
         )
     else:
         kwargs = {}
diff --git a/python/private/py_library.bzl b/python/private/py_library.bzl
index 6a65038..350ea35 100644
--- a/python/private/py_library.bzl
+++ b/python/private/py_library.bzl
@@ -102,6 +102,7 @@
     cc_info = semantics.get_cc_info_for_library(ctx)
     py_info, deps_transitive_sources, builtins_py_info = create_py_info(
         ctx,
+        original_sources = direct_sources,
         required_py_files = required_py_files,
         required_pyc_files = required_pyc_files,
         implicit_pyc_files = implicit_pyc_files,
diff --git a/tests/base_rules/base_tests.bzl b/tests/base_rules/base_tests.bzl
index 8e0d10d..a9fadd7 100644
--- a/tests/base_rules/base_tests.bzl
+++ b/tests/base_rules/base_tests.bzl
@@ -17,6 +17,7 @@
 load("@rules_testing//lib:truth.bzl", "matching")
 load("@rules_testing//lib:util.bzl", "PREVENT_IMPLICIT_BUILDING_TAGS", rt_util = "util")
 load("//python:py_info.bzl", "PyInfo")
+load("//python:py_library.bzl", "py_library")
 load("//python/private:reexports.bzl", "BuiltinPyInfo")  # buildifier: disable=bzl-visibility
 load("//tests/base_rules:util.bzl", pt_util = "util")
 load("//tests/support:py_info_subject.bzl", "py_info_subject")
@@ -58,6 +59,50 @@
     implementation = _not_produces_py_info_impl,
 )
 
+def _test_py_info_populated(name, config):
+    rt_util.helper_target(
+        config.base_test_rule,
+        name = name + "_subject",
+        srcs = [name + "_subject.py"],
+        pyi_srcs = ["subject.pyi"],
+        pyi_deps = [name + "_lib2"],
+    )
+    rt_util.helper_target(
+        py_library,
+        name = name + "_lib2",
+        srcs = ["lib2.py"],
+        pyi_srcs = ["lib2.pyi"],
+    )
+
+    analysis_test(
+        name = name,
+        target = name + "_subject",
+        impl = _test_py_info_populated_impl,
+    )
+
+def _test_py_info_populated_impl(env, target):
+    info = env.expect.that_target(target).provider(
+        PyInfo,
+        factory = py_info_subject,
+    )
+    info.direct_original_sources().contains_exactly([
+        "{package}/test_py_info_populated_subject.py",
+    ])
+    info.transitive_original_sources().contains_exactly([
+        "{package}/test_py_info_populated_subject.py",
+        "{package}/lib2.py",
+    ])
+
+    info.direct_pyi_files().contains_exactly([
+        "{package}/subject.pyi",
+    ])
+    info.transitive_pyi_files().contains_exactly([
+        "{package}/lib2.pyi",
+        "{package}/subject.pyi",
+    ])
+
+_tests.append(_test_py_info_populated)
+
 def _py_info_propagation_setup(name, config, produce_py_info_rule, test_impl):
     rt_util.helper_target(
         config.base_test_rule,
diff --git a/tests/base_rules/py_info/py_info_tests.bzl b/tests/base_rules/py_info/py_info_tests.bzl
index 4067a59..e160e70 100644
--- a/tests/base_rules/py_info/py_info_tests.bzl
+++ b/tests/base_rules/py_info/py_info_tests.bzl
@@ -24,9 +24,13 @@
 
 def _provide_py_info_impl(ctx):
     kwargs = {
+        "direct_original_sources": depset(ctx.files.direct_original_sources),
         "direct_pyc_files": depset(ctx.files.direct_pyc_files),
+        "direct_pyi_files": depset(ctx.files.direct_pyi_files),
         "imports": depset(ctx.attr.imports),
+        "transitive_original_sources": depset(ctx.files.transitive_original_sources),
         "transitive_pyc_files": depset(ctx.files.transitive_pyc_files),
+        "transitive_pyi_files": depset(ctx.files.transitive_pyi_files),
         "transitive_sources": depset(ctx.files.transitive_sources),
     }
     if ctx.attr.has_py2_only_sources != -1:
@@ -56,11 +60,15 @@
 provide_py_info = rule(
     implementation = _provide_py_info_impl,
     attrs = {
+        "direct_original_sources": attr.label_list(allow_files = True),
         "direct_pyc_files": attr.label_list(allow_files = True),
+        "direct_pyi_files": attr.label_list(allow_files = True),
         "has_py2_only_sources": attr.int(default = -1),
         "has_py3_only_sources": attr.int(default = -1),
         "imports": attr.string_list(),
+        "transitive_original_sources": attr.label_list(allow_files = True),
         "transitive_pyc_files": attr.label_list(allow_files = True),
+        "transitive_pyi_files": attr.label_list(allow_files = True),
         "transitive_sources": attr.label_list(allow_files = True),
     },
 )
@@ -109,7 +117,15 @@
     rt_util.helper_target(
         native.filegroup,
         name = name + "_misc",
-        srcs = ["trans.py", "direct.pyc", "trans.pyc"],
+        srcs = [
+            "trans.py",
+            "direct.pyc",
+            "trans.pyc",
+            "original.py",
+            "trans-original.py",
+            "direct.pyi",
+            "trans.pyi",
+        ],
     )
 
     py_info_targets = {}
@@ -123,6 +139,10 @@
             direct_pyc_files = ["py{}-direct.pyc".format(n)],
             imports = ["py{}import".format(n)],
             transitive_pyc_files = ["py{}-trans.pyc".format(n)],
+            direct_original_sources = ["py{}-original-direct.py".format(n)],
+            transitive_original_sources = ["py{}-original-trans.py".format(n)],
+            direct_pyi_files = ["py{}-direct.pyi".format(n)],
+            transitive_pyi_files = ["py{}-trans.pyi".format(n)],
         )
     analysis_test(
         name = name,
@@ -133,13 +153,25 @@
     )
 
 def _test_py_info_builder_impl(env, targets):
-    trans, direct_pyc, trans_pyc = targets.misc[DefaultInfo].files.to_list()
+    (
+        trans,
+        direct_pyc,
+        trans_pyc,
+        original_py,
+        trans_original_py,
+        direct_pyi,
+        trans_pyi,
+    ) = targets.misc[DefaultInfo].files.to_list()
     builder = PyInfoBuilder()
     builder.direct_pyc_files.add(direct_pyc)
+    builder.direct_original_sources.add(original_py)
+    builder.direct_pyi_files.add(direct_pyi)
     builder.merge_has_py2_only_sources(True)
     builder.merge_has_py3_only_sources(True)
     builder.imports.add("import-path")
     builder.transitive_pyc_files.add(trans_pyc)
+    builder.transitive_pyi_files.add(trans_pyi)
+    builder.transitive_original_sources.add(trans_original_py)
     builder.transitive_sources.add(trans)
     builder.merge_uses_shared_libraries(True)
 
@@ -174,6 +206,8 @@
             "py5import",
             "py6import",
         ])
+
+        # Checks for non-Bazel builtin PyInfo
         if hasattr(actual, "direct_pyc_files"):
             subject.direct_pyc_files().contains_exactly([
                 "tests/base_rules/py_info/direct.pyc",
@@ -189,6 +223,34 @@
                 "tests/base_rules/py_info/py5-trans.pyc",
                 "tests/base_rules/py_info/py6-trans.pyc",
             ])
+            subject.direct_original_sources().contains_exactly([
+                "tests/base_rules/py_info/original.py",
+                "tests/base_rules/py_info/py4-original-direct.py",
+                "tests/base_rules/py_info/py6-original-direct.py",
+            ])
+            subject.transitive_original_sources().contains_exactly([
+                "tests/base_rules/py_info/trans-original.py",
+                "tests/base_rules/py_info/py1-original-trans.py",
+                "tests/base_rules/py_info/py2-original-trans.py",
+                "tests/base_rules/py_info/py3-original-trans.py",
+                "tests/base_rules/py_info/py4-original-trans.py",
+                "tests/base_rules/py_info/py5-original-trans.py",
+                "tests/base_rules/py_info/py6-original-trans.py",
+            ])
+            subject.direct_pyi_files().contains_exactly([
+                "tests/base_rules/py_info/direct.pyi",
+                "tests/base_rules/py_info/py4-direct.pyi",
+                "tests/base_rules/py_info/py6-direct.pyi",
+            ])
+            subject.transitive_pyi_files().contains_exactly([
+                "tests/base_rules/py_info/trans.pyi",
+                "tests/base_rules/py_info/py1-trans.pyi",
+                "tests/base_rules/py_info/py2-trans.pyi",
+                "tests/base_rules/py_info/py3-trans.pyi",
+                "tests/base_rules/py_info/py4-trans.pyi",
+                "tests/base_rules/py_info/py5-trans.pyi",
+                "tests/base_rules/py_info/py6-trans.pyi",
+            ])
 
     check(builder.build())
     if BuiltinPyInfo != None:
diff --git a/tests/support/py_info_subject.bzl b/tests/support/py_info_subject.bzl
index bfed0b3..9122eaa 100644
--- a/tests/support/py_info_subject.bzl
+++ b/tests/support/py_info_subject.bzl
@@ -31,11 +31,15 @@
     # buildifier: disable=uninitialized
     public = struct(
         # go/keep-sorted start
+        direct_original_sources = lambda *a, **k: _py_info_subject_direct_original_sources(self, *a, **k),
         direct_pyc_files = lambda *a, **k: _py_info_subject_direct_pyc_files(self, *a, **k),
+        direct_pyi_files = lambda *a, **k: _py_info_subject_direct_pyi_files(self, *a, **k),
         has_py2_only_sources = lambda *a, **k: _py_info_subject_has_py2_only_sources(self, *a, **k),
         has_py3_only_sources = lambda *a, **k: _py_info_subject_has_py3_only_sources(self, *a, **k),
         imports = lambda *a, **k: _py_info_subject_imports(self, *a, **k),
+        transitive_original_sources = lambda *a, **k: _py_info_subject_transitive_original_sources(self, *a, **k),
         transitive_pyc_files = lambda *a, **k: _py_info_subject_transitive_pyc_files(self, *a, **k),
+        transitive_pyi_files = lambda *a, **k: _py_info_subject_transitive_pyi_files(self, *a, **k),
         transitive_sources = lambda *a, **k: _py_info_subject_transitive_sources(self, *a, **k),
         uses_shared_libraries = lambda *a, **k: _py_info_subject_uses_shared_libraries(self, *a, **k),
         # go/keep-sorted end
@@ -46,6 +50,14 @@
     )
     return public
 
+def _py_info_subject_direct_original_sources(self):
+    """Returns a `DepsetFileSubject` for the `direct_original_sources` attribute.
+    """
+    return subjects.depset_file(
+        self.actual.direct_original_sources,
+        meta = self.meta.derive("direct_original_sources()"),
+    )
+
 def _py_info_subject_direct_pyc_files(self):
     """Returns a `DepsetFileSubject` for the `direct_pyc_files` attribute.
 
@@ -56,6 +68,14 @@
         meta = self.meta.derive("direct_pyc_files()"),
     )
 
+def _py_info_subject_direct_pyi_files(self):
+    """Returns a `DepsetFileSubject` for the `direct_pyi_files` attribute.
+    """
+    return subjects.depset_file(
+        self.actual.direct_pyi_files,
+        meta = self.meta.derive("direct_pyi_files()"),
+    )
+
 def _py_info_subject_has_py2_only_sources(self):
     """Returns a `BoolSubject` for the `has_py2_only_sources` attribute.
 
@@ -86,6 +106,16 @@
         meta = self.meta.derive("imports()"),
     )
 
+def _py_info_subject_transitive_original_sources(self):
+    """Returns a `DepsetFileSubject` for the `transitive_original_sources` attribute.
+
+    Method: PyInfoSubject.transitive_original_sources
+    """
+    return subjects.depset_file(
+        self.actual.transitive_original_sources,
+        meta = self.meta.derive("transitive_original_sources()"),
+    )
+
 def _py_info_subject_transitive_pyc_files(self):
     """Returns a `DepsetFileSubject` for the `transitive_pyc_files` attribute.
 
@@ -96,6 +126,14 @@
         meta = self.meta.derive("transitive_pyc_files()"),
     )
 
+def _py_info_subject_transitive_pyi_files(self):
+    """Returns a `DepsetFileSubject` for the `transitive_pyi_files` attribute.
+    """
+    return subjects.depset_file(
+        self.actual.transitive_pyi_files,
+        meta = self.meta.derive("transitive_pyi_files()"),
+    )
+
 def _py_info_subject_transitive_sources(self):
     """Returns a `DepsetFileSubject` for the `transitive_sources` attribute.