refactor: move PyInfo into separate file (#2249)

Both PyInfo and providers.bzl are fairly large, and some upcoming PRs
will be adding
more PyInfo-specific code. To keep them manageable, move PyInfo into its
own file.
diff --git a/python/BUILD.bazel b/python/BUILD.bazel
index b7a2172..53fb812 100644
--- a/python/BUILD.bazel
+++ b/python/BUILD.bazel
@@ -167,8 +167,8 @@
     name = "py_info_bzl",
     srcs = ["py_info.bzl"],
     deps = [
+        "//python/private:py_info_bzl",
         "//python/private:reexports_bzl",
-        "//python/private/common:providers_bzl",
         "@rules_python_internal//:rules_python_config_bzl",
     ],
 )
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index bfe3764..8479f67 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -267,6 +267,15 @@
 )
 
 bzl_library(
+    name = "py_info_bzl",
+    srcs = ["py_info.bzl"],
+    deps = [
+        ":reexports_bzl",
+        ":util_bzl",
+    ],
+)
+
+bzl_library(
     name = "py_interpreter_program_bzl",
     srcs = ["py_interpreter_program.bzl"],
     deps = ["@bazel_skylib//rules:common_settings"],
diff --git a/python/private/common/BUILD.bazel b/python/private/common/BUILD.bazel
index 805c002..6fef8e8 100644
--- a/python/private/common/BUILD.bazel
+++ b/python/private/common/BUILD.bazel
@@ -29,11 +29,11 @@
     srcs = ["attributes.bzl"],
     deps = [
         ":common_bzl",
-        ":providers_bzl",
         ":py_internal_bzl",
         ":semantics_bzl",
         "//python/private:enum_bzl",
         "//python/private:flags_bzl",
+        "//python/private:py_info_bzl",
         "//python/private:reexports_bzl",
         "//python/private:rules_cc_srcs_bzl",
         "@bazel_skylib//rules:common_settings",
@@ -68,6 +68,7 @@
         ":providers_bzl",
         ":py_internal_bzl",
         ":semantics_bzl",
+        "//python/private:py_info_bzl",
         "//python/private:reexports_bzl",
         "//python/private:rules_cc_srcs_bzl",
     ],
@@ -133,6 +134,7 @@
         ":py_internal_bzl",
         "//python/private:flags_bzl",
         "//python/private:py_executable_info_bzl",
+        "//python/private:py_info_bzl",
         "//python/private:rules_cc_srcs_bzl",
         "//python/private:toolchain_types_bzl",
         "@bazel_skylib//lib:dicts",
diff --git a/python/private/common/attributes.bzl b/python/private/common/attributes.bzl
index 90a5332..5e81f46 100644
--- a/python/private/common/attributes.bzl
+++ b/python/private/common/attributes.bzl
@@ -17,9 +17,9 @@
 load("@rules_cc//cc:defs.bzl", "CcInfo")
 load("//python/private:enum.bzl", "enum")
 load("//python/private:flags.bzl", "PrecompileFlag", "PrecompileSourceRetentionFlag")
+load("//python/private:py_info.bzl", "PyInfo")
 load("//python/private:reexports.bzl", "BuiltinPyInfo")
 load(":common.bzl", "union_attrs")
-load(":providers.bzl", "PyInfo")
 load(":py_internal.bzl", "py_internal")
 load(
     ":semantics.bzl",
diff --git a/python/private/common/common.bzl b/python/private/common/common.bzl
index 5559ccd..bbda712 100644
--- a/python/private/common/common.bzl
+++ b/python/private/common/common.bzl
@@ -13,9 +13,9 @@
 # limitations under the License.
 """Various things common to Bazel and Google rule implementations."""
 
+load("//python/private:py_info.bzl", "PyInfo")
 load("//python/private:reexports.bzl", "BuiltinPyInfo")
 load(":cc_helper.bzl", "cc_helper")
-load(":providers.bzl", "PyInfo")
 load(":py_internal.bzl", "py_internal")
 load(
     ":semantics.bzl",
diff --git a/python/private/common/providers.bzl b/python/private/common/providers.bzl
index eb8b910..b704ce0 100644
--- a/python/private/common/providers.bzl
+++ b/python/private/common/providers.bzl
@@ -14,7 +14,7 @@
 """Providers for Python rules."""
 
 load("@rules_cc//cc:defs.bzl", "CcInfo")
-load("//python/private:util.bzl", "IS_BAZEL_6_OR_HIGHER")
+load("//python/private:util.bzl", "define_bazel_6_provider")
 
 DEFAULT_STUB_SHEBANG = "#!/usr/bin/env python3"
 
@@ -22,18 +22,6 @@
 
 _PYTHON_VERSION_VALUES = ["PY2", "PY3"]
 
-# Helper to make the provider definitions not crash under Bazel 5.4:
-# Bazel 5.4 doesn't support the `init` arg of `provider()`, so we have to
-# not pass that when using Bazel 5.4. But, not passing the `init` arg
-# changes the return value from a two-tuple to a single value, which then
-# breaks Bazel 6+ code.
-# This isn't actually used under Bazel 5.4, so just stub out the values
-# to get past the loading phase.
-def _define_provider(doc, fields, **kwargs):
-    if not IS_BAZEL_6_OR_HIGHER:
-        return provider("Stub, not used", fields = []), None
-    return provider(doc = doc, fields = fields, **kwargs)
-
 def _optional_int(value):
     return int(value) if value != None else None
 
@@ -133,9 +121,7 @@
         "zip_main_template": zip_main_template,
     }
 
-# TODO(#15897): Rename this to PyRuntimeInfo when we're ready to replace the Java
-# implemented provider with the Starlark one.
-PyRuntimeInfo, _unused_raw_py_runtime_info_ctor = _define_provider(
+PyRuntimeInfo, _unused_raw_py_runtime_info_ctor = define_bazel_6_provider(
     doc = """Contains information about a Python runtime, as returned by the `py_runtime`
 rule.
 
@@ -314,102 +300,13 @@
     },
 )
 
-def _check_arg_type(name, required_type, value):
-    value_type = type(value)
-    if value_type != required_type:
-        fail("parameter '{}' got value of type '{}', want '{}'".format(
-            name,
-            value_type,
-            required_type,
-        ))
-
-def _PyInfo_init(
-        *,
-        transitive_sources,
-        uses_shared_libraries = False,
-        imports = depset(),
-        has_py2_only_sources = False,
-        has_py3_only_sources = False,
-        direct_pyc_files = depset(),
-        transitive_pyc_files = depset()):
-    _check_arg_type("transitive_sources", "depset", transitive_sources)
-
-    # Verify it's postorder compatible, but retain is original ordering.
-    depset(transitive = [transitive_sources], order = "postorder")
-
-    _check_arg_type("uses_shared_libraries", "bool", uses_shared_libraries)
-    _check_arg_type("imports", "depset", imports)
-    _check_arg_type("has_py2_only_sources", "bool", has_py2_only_sources)
-    _check_arg_type("has_py3_only_sources", "bool", has_py3_only_sources)
-    _check_arg_type("direct_pyc_files", "depset", direct_pyc_files)
-    _check_arg_type("transitive_pyc_files", "depset", transitive_pyc_files)
-    return {
-        "direct_pyc_files": direct_pyc_files,
-        "has_py2_only_sources": has_py2_only_sources,
-        "has_py3_only_sources": has_py2_only_sources,
-        "imports": imports,
-        "transitive_pyc_files": transitive_pyc_files,
-        "transitive_sources": transitive_sources,
-        "uses_shared_libraries": uses_shared_libraries,
-    }
-
-PyInfo, _unused_raw_py_info_ctor = _define_provider(
-    doc = "Encapsulates information provided by the Python rules.",
-    init = _PyInfo_init,
-    fields = {
-        "direct_pyc_files": """
-:type: depset[File]
-
-Precompiled Python files that are considered directly provided
-by the target.
-""",
-        "has_py2_only_sources": """
-:type: bool
-
-Whether any of this target's transitive sources requires a Python 2 runtime.
-""",
-        "has_py3_only_sources": """
-:type: bool
-
-Whether any of this target's transitive sources requires a Python 3 runtime.
-""",
-        "imports": """\
-:type: depset[str]
-
-A depset of import path strings to be added to the `PYTHONPATH` of executable
-Python targets. These are accumulated from the transitive `deps`.
-The order of the depset is not guaranteed and may be changed in the future. It
-is recommended to use `default` order (the default).
-""",
-        "transitive_pyc_files": """
-:type: depset[File]
-
-Direct and transitive precompiled Python files that are provided by the target.
-""",
-        "transitive_sources": """\
-:type: depset[File]
-
-A (`postorder`-compatible) depset of `.py` files appearing in the target's
-`srcs` and the `srcs` of the target's transitive `deps`.
-""",
-        "uses_shared_libraries": """
-:type: bool
-
-Whether any of this target's transitive `deps` has a shared library file (such
-as a `.so` file).
-
-This field is currently unused in Bazel and may go away in the future.
-""",
-    },
-)
-
 def _PyCcLinkParamsProvider_init(cc_info):
     return {
         "cc_info": CcInfo(linking_context = cc_info.linking_context),
     }
 
 # buildifier: disable=name-conventions
-PyCcLinkParamsProvider, _unused_raw_py_cc_link_params_provider_ctor = _define_provider(
+PyCcLinkParamsProvider, _unused_raw_py_cc_link_params_provider_ctor = define_bazel_6_provider(
     doc = ("Python-wrapper to forward {obj}`CcInfo.linking_context`. This is to " +
            "allow Python targets to propagate C++ linking information, but " +
            "without the Python target appearing to be a valid C++ rule dependency"),
diff --git a/python/private/common/py_executable.bzl b/python/private/common/py_executable.bzl
index 80418ac..37ca313 100644
--- a/python/private/common/py_executable.bzl
+++ b/python/private/common/py_executable.bzl
@@ -19,6 +19,7 @@
 load("@rules_cc//cc:defs.bzl", "cc_common")
 load("//python/private:flags.bzl", "PrecompileAddToRunfilesFlag")
 load("//python/private:py_executable_info.bzl", "PyExecutableInfo")
+load("//python/private:py_info.bzl", "PyInfo")
 load("//python/private:reexports.bzl", "BuiltinPyRuntimeInfo")
 load(
     "//python/private:toolchain_types.bzl",
@@ -52,7 +53,6 @@
 load(
     ":providers.bzl",
     "PyCcLinkParamsProvider",
-    "PyInfo",
     "PyRuntimeInfo",
 )
 load(":py_internal.bzl", "py_internal")
diff --git a/python/private/py_info.bzl b/python/private/py_info.bzl
new file mode 100644
index 0000000..7945775
--- /dev/null
+++ b/python/private/py_info.bzl
@@ -0,0 +1,115 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Implementation of PyInfo provider and PyInfo-specific utilities."""
+
+load(":util.bzl", "define_bazel_6_provider")
+
+def _check_arg_type(name, required_type, value):
+    """Check that a value is of an expected type."""
+    value_type = type(value)
+    if value_type != required_type:
+        fail("parameter '{}' got value of type '{}', want '{}'".format(
+            name,
+            value_type,
+            required_type,
+        ))
+
+def _PyInfo_init(
+        *,
+        transitive_sources,
+        uses_shared_libraries = False,
+        imports = depset(),
+        has_py2_only_sources = False,
+        has_py3_only_sources = False,
+        direct_pyc_files = depset(),
+        transitive_pyc_files = depset()):
+    _check_arg_type("transitive_sources", "depset", transitive_sources)
+
+    # Verify it's postorder compatible, but retain is original ordering.
+    depset(transitive = [transitive_sources], order = "postorder")
+
+    _check_arg_type("uses_shared_libraries", "bool", uses_shared_libraries)
+    _check_arg_type("imports", "depset", imports)
+    _check_arg_type("has_py2_only_sources", "bool", has_py2_only_sources)
+    _check_arg_type("has_py3_only_sources", "bool", has_py3_only_sources)
+    _check_arg_type("direct_pyc_files", "depset", direct_pyc_files)
+    _check_arg_type("transitive_pyc_files", "depset", transitive_pyc_files)
+
+    return {
+        "direct_pyc_files": direct_pyc_files,
+        "has_py2_only_sources": has_py2_only_sources,
+        "has_py3_only_sources": has_py2_only_sources,
+        "imports": imports,
+        "transitive_pyc_files": transitive_pyc_files,
+        "transitive_sources": transitive_sources,
+        "uses_shared_libraries": uses_shared_libraries,
+    }
+
+PyInfo, _unused_raw_py_info_ctor = define_bazel_6_provider(
+    doc = "Encapsulates information provided by the Python rules.",
+    init = _PyInfo_init,
+    fields = {
+        "direct_pyc_files": """
+:type: depset[File]
+
+Precompiled Python files that are considered directly provided
+by the target and **must be included**.
+
+These files usually come from, e.g., a library setting {attr}`precompile=enabled`
+to forcibly enable precompiling for itself. Downstream binaries are expected
+to always include these files, as the originating target expects them to exist.
+""",
+        "has_py2_only_sources": """
+:type: bool
+
+Whether any of this target's transitive sources requires a Python 2 runtime.
+""",
+        "has_py3_only_sources": """
+:type: bool
+
+Whether any of this target's transitive sources requires a Python 3 runtime.
+""",
+        "imports": """\
+:type: depset[str]
+
+A depset of import path strings to be added to the `PYTHONPATH` of executable
+Python targets. These are accumulated from the transitive `deps`.
+The order of the depset is not guaranteed and may be changed in the future. It
+is recommended to use `default` order (the default).
+""",
+        "transitive_pyc_files": """
+:type: depset[File]
+
+The transitive set of precompiled files that must be included.
+
+These files usually come from, e.g., a library setting {attr}`precompile=enabled`
+to forcibly enable precompiling for itself. Downstream binaries are expected
+to always include these files, as the originating target expects them to exist.
+""",
+        "transitive_sources": """\
+:type: depset[File]
+
+A (`postorder`-compatible) depset of `.py` files appearing in the target's
+`srcs` and the `srcs` of the target's transitive `deps`.
+""",
+        "uses_shared_libraries": """
+:type: bool
+
+Whether any of this target's transitive `deps` has a shared library file (such
+as a `.so` file).
+
+This field is currently unused in Bazel and may go away in the future.
+""",
+    },
+)
diff --git a/python/private/util.bzl b/python/private/util.bzl
index 16b8ff8..3c32adc 100644
--- a/python/private/util.bzl
+++ b/python/private/util.bzl
@@ -84,6 +84,19 @@
     else:
         attrs["tags"] = [tag]
 
+# Helper to make the provider definitions not crash under Bazel 5.4:
+# Bazel 5.4 doesn't support the `init` arg of `provider()`, so we have to
+# not pass that when using Bazel 5.4. But, not passing the `init` arg
+# changes the return value from a two-tuple to a single value, which then
+# breaks Bazel 6+ code.
+# This isn't actually used under Bazel 5.4, so just stub out the values
+# to get past the loading phase.
+def define_bazel_6_provider(doc, fields, **kwargs):
+    """Define a provider, or a stub for pre-Bazel 7."""
+    if not IS_BAZEL_6_OR_HIGHER:
+        return provider("Stub, not used", fields = []), None
+    return provider(doc = doc, fields = fields, **kwargs)
+
 IS_BAZEL_7_OR_HIGHER = hasattr(native, "starlark_doc_extract")
 
 # Bazel 5.4 has a bug where every access of testing.ExecutionInfo is a
diff --git a/python/py_info.bzl b/python/py_info.bzl
index 0af35ac..52a66a8 100644
--- a/python/py_info.bzl
+++ b/python/py_info.bzl
@@ -15,7 +15,7 @@
 """Public entry point for PyInfo."""
 
 load("@rules_python_internal//:rules_python_config.bzl", "config")
+load("//python/private:py_info.bzl", _starlark_PyInfo = "PyInfo")
 load("//python/private:reexports.bzl", "BuiltinPyInfo")
-load("//python/private/common:providers.bzl", _starlark_PyInfo = "PyInfo")
 
 PyInfo = _starlark_PyInfo if config.enable_pystar else BuiltinPyInfo