From 188598ab8348ac1d84e417a66ebb8501506041e6 Mon Sep 17 00:00:00 2001
From: Ignas Anikevicius <240938+aignas@users.noreply.github.com>
Date: Wed, 22 Jan 2025 11:31:12 +0900
Subject: [PATCH] chore: remove internal usage of deprecated py_binary ad
 py_test (#2569)

This goes together with #2565 to remove the internal usage of the
deprecated
symbols. This also fixes the compile_pip_requirements symbol to print
the
correct deprecation message.

Builds on top of 611eda8

The example message that would be printed is as follows:
```
The 'py_test' symbol in '@+python+python_3_11//:defs.bzl'
is deprecated. It is an alias to the regular rule; use it directly instead:

load("@rules_python//python:py_test.bzl", "py_test")

py_test(
    name = "versioned_py_test",
    srcs = ["dummy.py"],
    main = "dummy.py",
    python_version = "3.11.11",
)
```

---------

Co-authored-by: Richard Levasseur <richardlev@gmail.com>
---
 CHANGELOG.md                                  |  3 +
 MODULE.bazel                                  |  7 +-
 WORKSPACE                                     |  4 +-
 docs/environment-variables.md                 |  6 ++
 examples/bzlmod/other_module/BUILD.bazel      |  5 +-
 .../other_module/other_module/pkg/BUILD.bazel |  8 +-
 examples/bzlmod/tests/BUILD.bazel             | 39 ++++----
 .../requirements/BUILD.bazel                  | 17 ++--
 .../multi_python_versions/tests/BUILD.bazel   | 43 +++++----
 python/config_settings/transition.bzl         | 30 +++---
 python/private/BUILD.bazel                    |  8 ++
 python/private/deprecation.bzl                | 59 ++++++++++++
 python/private/internal_config_repo.bzl       |  8 +-
 python/private/toolchains_repo.bzl            | 90 +++++++++--------
 python/uv/private/lock.bzl                    |  3 +-
 .../transition/multi_version_tests.bzl        |  9 +-
 tests/deprecated/BUILD.bazel                  | 96 +++++++++++++++++++
 tests/deprecated/dummy.py                     |  0
 tests/deprecated/requirements.in              |  0
 tests/deprecated/requirements.txt             |  6 ++
 tests/deprecated/requirements_hub.txt         |  6 ++
 tools/publish/BUILD.bazel                     |  8 +-
 22 files changed, 336 insertions(+), 119 deletions(-)
 create mode 100644 python/private/deprecation.bzl
 create mode 100644 tests/deprecated/BUILD.bazel
 create mode 100644 tests/deprecated/dummy.py
 create mode 100644 tests/deprecated/requirements.in
 create mode 100644 tests/deprecated/requirements.txt
 create mode 100644 tests/deprecated/requirements_hub.txt

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2d8c1631ed..00624db01f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -54,6 +54,9 @@ Unreleased changes template.
 ### Changed
 * (pypi) {obj}`pip.override` will now be ignored instead of raising an error,
   fixes [#2550](https://github.com/bazelbuild/rules_python/issues/2550).
+* (rules) deprecation warnings for deprecated symbols have been turned off by
+  default for now and can be enabled with `RULES_PYTHON_DEPRECATION_WARNINGS`
+  env var.
 
 {#v0-0-0-fixed}
 ### Fixed
diff --git a/MODULE.bazel b/MODULE.bazel
index 57780b2369..2ac5a27223 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -46,7 +46,12 @@ python.toolchain(
     is_default = True,
     python_version = "3.11",
 )
-use_repo(python, "python_3_11", "python_versions", "pythons_hub")
+use_repo(
+    python,
+    "python_3_11",
+    "pythons_hub",
+    python = "python_versions",
+)
 
 # This call registers the Python toolchains.
 register_toolchains("@pythons_hub//:all")
diff --git a/WORKSPACE b/WORKSPACE
index 7303b480f2..902af58ec8 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -68,12 +68,12 @@ load("//:internal_dev_setup.bzl", "rules_python_internal_setup")
 
 rules_python_internal_setup()
 
-load("@pythons_hub//:versions.bzl", "MINOR_MAPPING", "PYTHON_VERSIONS")
+load("@pythons_hub//:versions.bzl", "PYTHON_VERSIONS")
 load("//python:repositories.bzl", "python_register_multi_toolchains")
 
 python_register_multi_toolchains(
     name = "python",
-    default_version = MINOR_MAPPING.values()[-3],  # Use 3.11.10
+    default_version = "3.11",
     # Integration tests verify each version, so register all of them.
     python_versions = PYTHON_VERSIONS,
 )
diff --git a/docs/environment-variables.md b/docs/environment-variables.md
index 12c1bcf0c2..fb9971597b 100644
--- a/docs/environment-variables.md
+++ b/docs/environment-variables.md
@@ -40,6 +40,12 @@ When `1`, bzlmod extensions will print debug information about what they're
 doing. This is mostly useful for development to debug errors.
 :::
 
+:::{envvar} RULES_PYTHON_DEPRECATION_WARNINGS
+
+When `1`, the rules_python will warn users about deprecated functionality that will
+be removed in a subsequent major `rules_python` version. Defaults to `0` if unset.
+:::
+
 :::{envvar} RULES_PYTHON_ENABLE_PYSTAR
 
 When `1`, the rules_python Starlark implementation of the core rules is used
diff --git a/examples/bzlmod/other_module/BUILD.bazel b/examples/bzlmod/other_module/BUILD.bazel
index a93b92aaed..6294c5b0ae 100644
--- a/examples/bzlmod/other_module/BUILD.bazel
+++ b/examples/bzlmod/other_module/BUILD.bazel
@@ -1,9 +1,10 @@
-load("@python_versions//3.11:defs.bzl", compile_pip_requirements_311 = "compile_pip_requirements")
+load("@rules_python//python:pip.bzl", "compile_pip_requirements")
 
 # NOTE: To update the requirements, you need to uncomment the rules_python
 # override in the MODULE.bazel.
-compile_pip_requirements_311(
+compile_pip_requirements(
     name = "requirements",
     src = "requirements.in",
+    python_version = "3.11",
     requirements_txt = "requirements_lock_3_11.txt",
 )
diff --git a/examples/bzlmod/other_module/other_module/pkg/BUILD.bazel b/examples/bzlmod/other_module/other_module/pkg/BUILD.bazel
index 4fe392841e..53344c708a 100644
--- a/examples/bzlmod/other_module/other_module/pkg/BUILD.bazel
+++ b/examples/bzlmod/other_module/other_module/pkg/BUILD.bazel
@@ -1,7 +1,4 @@
-load(
-    "@python_3_11//:defs.bzl",
-    py_binary_311 = "py_binary",
-)
+load("@rules_python//python:py_binary.bzl", "py_binary")
 load("@rules_python//python:py_library.bzl", "py_library")
 
 py_library(
@@ -15,11 +12,12 @@ py_library(
 # This is used for testing mulitple versions of Python. This is
 # used only when you need to support multiple versions of Python
 # in the same project.
-py_binary_311(
+py_binary(
     name = "bin",
     srcs = ["bin.py"],
     data = ["data/data.txt"],
     main = "bin.py",
+    python_version = "3.11",
     visibility = ["//visibility:public"],
     deps = [
         ":lib",
diff --git a/examples/bzlmod/tests/BUILD.bazel b/examples/bzlmod/tests/BUILD.bazel
index dd50cf3294..4650fb8788 100644
--- a/examples/bzlmod/tests/BUILD.bazel
+++ b/examples/bzlmod/tests/BUILD.bazel
@@ -1,10 +1,6 @@
-load("@python_versions//3.10:defs.bzl", py_binary_3_10 = "py_binary", py_test_3_10 = "py_test")
-load("@python_versions//3.11:defs.bzl", py_binary_3_11 = "py_binary", py_test_3_11 = "py_test")
-load("@python_versions//3.9:defs.bzl", py_binary_3_9 = "py_binary", py_test_3_9 = "py_test")
 load("@pythons_hub//:versions.bzl", "MINOR_MAPPING")
 load("@rules_python//python:py_binary.bzl", "py_binary")
 load("@rules_python//python:py_test.bzl", "py_test")
-load("@rules_python//python/config_settings:transition.bzl", py_versioned_binary = "py_binary", py_versioned_test = "py_test")
 load("@rules_shell//shell:sh_test.bzl", "sh_test")
 
 py_binary(
@@ -13,25 +9,28 @@ py_binary(
     main = "version.py",
 )
 
-py_binary_3_9(
+py_binary(
     name = "version_3_9",
     srcs = ["version.py"],
     main = "version.py",
+    python_version = "3.9",
 )
 
-py_binary_3_10(
+py_binary(
     name = "version_3_10",
     srcs = ["version.py"],
     main = "version.py",
+    python_version = "3.10",
 )
 
-py_binary_3_11(
+py_binary(
     name = "version_3_11",
     srcs = ["version.py"],
     main = "version.py",
+    python_version = "3.11",
 )
 
-py_versioned_binary(
+py_binary(
     name = "version_3_10_versioned",
     srcs = ["version.py"],
     main = "version.py",
@@ -49,21 +48,23 @@ py_test(
     deps = ["//libs/my_lib"],
 )
 
-py_test_3_9(
+py_test(
     name = "my_lib_3_9_test",
     srcs = ["my_lib_test.py"],
     main = "my_lib_test.py",
+    python_version = "3.9",
     deps = ["//libs/my_lib"],
 )
 
-py_test_3_10(
+py_test(
     name = "my_lib_3_10_test",
     srcs = ["my_lib_test.py"],
     main = "my_lib_test.py",
+    python_version = "3.10",
     deps = ["//libs/my_lib"],
 )
 
-py_versioned_test(
+py_test(
     name = "my_lib_versioned_test",
     srcs = ["my_lib_test.py"],
     main = "my_lib_test.py",
@@ -92,21 +93,23 @@ py_test(
     main = "version_test.py",
 )
 
-py_test_3_9(
+py_test(
     name = "version_3_9_test",
     srcs = ["version_test.py"],
     env = {"VERSION_CHECK": "3.9"},
     main = "version_test.py",
+    python_version = "3.9",
 )
 
-py_test_3_10(
+py_test(
     name = "version_3_10_test",
     srcs = ["version_test.py"],
     env = {"VERSION_CHECK": "3.10"},
     main = "version_test.py",
+    python_version = "3.10",
 )
 
-py_versioned_test(
+py_test(
     name = "version_versioned_test",
     srcs = ["version_test.py"],
     env = {"VERSION_CHECK": "3.10"},
@@ -114,11 +117,12 @@ py_versioned_test(
     python_version = "3.10",
 )
 
-py_test_3_11(
+py_test(
     name = "version_3_11_test",
     srcs = ["version_test.py"],
     env = {"VERSION_CHECK": "3.11"},
     main = "version_test.py",
+    python_version = "3.11",
 )
 
 py_test(
@@ -133,7 +137,7 @@ py_test(
     main = "cross_version_test.py",
 )
 
-py_test_3_10(
+py_test(
     name = "version_3_10_takes_3_9_subprocess_test",
     srcs = ["cross_version_test.py"],
     data = [":version_3_9"],
@@ -143,9 +147,10 @@ py_test_3_10(
         "VERSION_CHECK": "3.10",
     },
     main = "cross_version_test.py",
+    python_version = "3.10",
 )
 
-py_versioned_test(
+py_test(
     name = "version_3_10_takes_3_9_subprocess_test_2",
     srcs = ["cross_version_test.py"],
     data = [":version_3_9"],
diff --git a/examples/multi_python_versions/requirements/BUILD.bazel b/examples/multi_python_versions/requirements/BUILD.bazel
index f67333a657..c9b695e8e4 100644
--- a/examples/multi_python_versions/requirements/BUILD.bazel
+++ b/examples/multi_python_versions/requirements/BUILD.bazel
@@ -1,28 +1,29 @@
-load("@python//3.10:defs.bzl", compile_pip_requirements_3_10 = "compile_pip_requirements")
-load("@python//3.11:defs.bzl", compile_pip_requirements_3_11 = "compile_pip_requirements")
-load("@python//3.8:defs.bzl", compile_pip_requirements_3_8 = "compile_pip_requirements")
-load("@python//3.9:defs.bzl", compile_pip_requirements_3_9 = "compile_pip_requirements")
+load("@rules_python//python:pip.bzl", "compile_pip_requirements")
 
-compile_pip_requirements_3_8(
+compile_pip_requirements(
     name = "requirements_3_8",
     src = "requirements.in",
+    python_version = "3.8",
     requirements_txt = "requirements_lock_3_8.txt",
 )
 
-compile_pip_requirements_3_9(
+compile_pip_requirements(
     name = "requirements_3_9",
     src = "requirements.in",
+    python_version = "3.9",
     requirements_txt = "requirements_lock_3_9.txt",
 )
 
-compile_pip_requirements_3_10(
+compile_pip_requirements(
     name = "requirements_3_10",
     src = "requirements.in",
+    python_version = "3.10",
     requirements_txt = "requirements_lock_3_10.txt",
 )
 
-compile_pip_requirements_3_11(
+compile_pip_requirements(
     name = "requirements_3_11",
     src = "requirements.in",
+    python_version = "3.11",
     requirements_txt = "requirements_lock_3_11.txt",
 )
diff --git a/examples/multi_python_versions/tests/BUILD.bazel b/examples/multi_python_versions/tests/BUILD.bazel
index d04ac6bb0a..e3dfb48cca 100644
--- a/examples/multi_python_versions/tests/BUILD.bazel
+++ b/examples/multi_python_versions/tests/BUILD.bazel
@@ -1,10 +1,6 @@
 load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
 load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
 load("@bazel_skylib//rules:write_file.bzl", "write_file")
-load("@python//3.10:defs.bzl", py_binary_3_10 = "py_binary", py_test_3_10 = "py_test")
-load("@python//3.11:defs.bzl", py_binary_3_11 = "py_binary", py_test_3_11 = "py_test")
-load("@python//3.8:defs.bzl", py_binary_3_8 = "py_binary", py_test_3_8 = "py_test")
-load("@python//3.9:defs.bzl", py_binary_3_9 = "py_binary", py_test_3_9 = "py_test")
 load("@pythons_hub//:versions.bzl", "MINOR_MAPPING", "PYTHON_VERSIONS")
 load("@rules_python//python:py_binary.bzl", "py_binary")
 load("@rules_python//python:py_test.bzl", "py_test")
@@ -26,28 +22,32 @@ py_binary(
     srcs = ["version_default.py"],
 )
 
-py_binary_3_8(
+py_binary(
     name = "version_3_8",
     srcs = ["version.py"],
     main = "version.py",
+    python_version = "3.8",
 )
 
-py_binary_3_9(
+py_binary(
     name = "version_3_9",
     srcs = ["version.py"],
     main = "version.py",
+    python_version = "3.9",
 )
 
-py_binary_3_10(
+py_binary(
     name = "version_3_10",
     srcs = ["version.py"],
     main = "version.py",
+    python_version = "3.10",
 )
 
-py_binary_3_11(
+py_binary(
     name = "version_3_11",
     srcs = ["version.py"],
     main = "version.py",
+    python_version = "3.11",
 )
 
 py_test(
@@ -57,31 +57,35 @@ py_test(
     deps = ["//libs/my_lib"],
 )
 
-py_test_3_8(
+py_test(
     name = "my_lib_3_8_test",
     srcs = ["my_lib_test.py"],
     main = "my_lib_test.py",
+    python_version = "3.8",
     deps = ["//libs/my_lib"],
 )
 
-py_test_3_9(
+py_test(
     name = "my_lib_3_9_test",
     srcs = ["my_lib_test.py"],
     main = "my_lib_test.py",
+    python_version = "3.9",
     deps = ["//libs/my_lib"],
 )
 
-py_test_3_10(
+py_test(
     name = "my_lib_3_10_test",
     srcs = ["my_lib_test.py"],
     main = "my_lib_test.py",
+    python_version = "3.10",
     deps = ["//libs/my_lib"],
 )
 
-py_test_3_11(
+py_test(
     name = "my_lib_3_11_test",
     srcs = ["my_lib_test.py"],
     main = "my_lib_test.py",
+    python_version = "3.11",
     deps = ["//libs/my_lib"],
 )
 
@@ -98,32 +102,36 @@ py_test(
     env = {"VERSION_CHECK": "3.9"},  # The default defined in the WORKSPACE.
 )
 
-py_test_3_8(
+py_test(
     name = "version_3_8_test",
     srcs = ["version_test.py"],
     env = {"VERSION_CHECK": "3.8"},
     main = "version_test.py",
+    python_version = "3.8",
 )
 
-py_test_3_9(
+py_test(
     name = "version_3_9_test",
     srcs = ["version_test.py"],
     env = {"VERSION_CHECK": "3.9"},
     main = "version_test.py",
+    python_version = "3.9",
 )
 
-py_test_3_10(
+py_test(
     name = "version_3_10_test",
     srcs = ["version_test.py"],
     env = {"VERSION_CHECK": "3.10"},
     main = "version_test.py",
+    python_version = "3.10",
 )
 
-py_test_3_11(
+py_test(
     name = "version_3_11_test",
     srcs = ["version_test.py"],
     env = {"VERSION_CHECK": "3.11"},
     main = "version_test.py",
+    python_version = "3.11",
 )
 
 py_test(
@@ -138,7 +146,7 @@ py_test(
     main = "cross_version_test.py",
 )
 
-py_test_3_10(
+py_test(
     name = "version_3_10_takes_3_9_subprocess_test",
     srcs = ["cross_version_test.py"],
     data = [":version_3_9"],
@@ -148,6 +156,7 @@ py_test_3_10(
         "VERSION_CHECK": "3.10",
     },
     main = "cross_version_test.py",
+    python_version = "3.10",
 )
 
 sh_test(
diff --git a/python/config_settings/transition.bzl b/python/config_settings/transition.bzl
index c241f20746..937f33bb88 100644
--- a/python/config_settings/transition.bzl
+++ b/python/config_settings/transition.bzl
@@ -23,12 +23,18 @@ of them should be changed to load the regular rules directly.
 
 load("//python:py_binary.bzl", _py_binary = "py_binary")
 load("//python:py_test.bzl", _py_test = "py_test")
-
-_DEPRECATION_MESSAGE = """
-The {name} symbol in @rules_python//python/config_settings:transition.bzl
-is deprecated. It is an alias to the regular rule; use it directly instead:
-    load("@rules_python//python:{name}.bzl", "{name}")
-"""
+load("//python/private:deprecation.bzl", "with_deprecation")
+load("//python/private:text_util.bzl", "render")
+
+def _with_deprecation(kwargs, *, name, python_version):
+    kwargs["python_version"] = python_version
+    return with_deprecation.symbol(
+        kwargs,
+        symbol_name = name,
+        old_load = "@rules_python//python/config_settings:transition.bzl",
+        new_load = "@rules_python//python:{}.bzl".format(name),
+        snippet = render.call(name, **{k: repr(v) for k, v in kwargs.items()}),
+    )
 
 def py_binary(**kwargs):
     """[DEPRECATED] Deprecated alias for py_binary.
@@ -37,11 +43,7 @@ def py_binary(**kwargs):
         **kwargs: keyword args forwarded onto {obj}`py_binary`.
     """
 
-    deprecation = _DEPRECATION_MESSAGE.format(name = "py_binary")
-    if kwargs.get("deprecation"):
-        deprecation = kwargs.get("deprecation") + "\n\n" + deprecation
-    kwargs["deprecation"] = deprecation
-    _py_binary(**kwargs)
+    _py_binary(**_with_deprecation(kwargs, name = "py_binary", python_version = kwargs.get("python_version")))
 
 def py_test(**kwargs):
     """[DEPRECATED] Deprecated alias for py_test.
@@ -49,8 +51,4 @@ def py_test(**kwargs):
     Args:
         **kwargs: keyword args forwarded onto {obj}`py_binary`.
     """
-    deprecation = _DEPRECATION_MESSAGE.format(name = "py_test")
-    if kwargs.get("deprecation"):
-        deprecation = kwargs.get("deprecation") + "\n\n" + deprecation
-    kwargs["deprecation"] = deprecation
-    _py_test(**kwargs)
+    _py_test(**_with_deprecation(kwargs, name = "py_test", python_version = kwargs.get("python_version")))
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index 706506a19c..14f52c541b 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -138,6 +138,14 @@ bzl_library(
     ],
 )
 
+bzl_library(
+    name = "deprecation_bzl",
+    srcs = ["deprecation.bzl"],
+    deps = [
+        "@rules_python_internal//:rules_python_config_bzl",
+    ],
+)
+
 bzl_library(
     name = "enum_bzl",
     srcs = ["enum.bzl"],
diff --git a/python/private/deprecation.bzl b/python/private/deprecation.bzl
new file mode 100644
index 0000000000..70461c2fa1
--- /dev/null
+++ b/python/private/deprecation.bzl
@@ -0,0 +1,59 @@
+# 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.
+
+"""Helper functions to deprecation utilities.
+"""
+
+load("@rules_python_internal//:rules_python_config.bzl", "config")
+
+_DEPRECATION_MESSAGE = """
+The '{name}' symbol in '{old_load}'
+is deprecated. It is an alias to the regular rule; use it directly instead:
+
+load("{new_load}", "{name}")
+
+{snippet}
+"""
+
+def _symbol(kwargs, *, symbol_name, new_load, old_load, snippet = ""):
+    """An internal function to propagate the deprecation warning.
+
+    This is not an API that should be used outside `rules_python`.
+
+    Args:
+        kwargs: Arguments to modify.
+        symbol_name: {type}`str` the symbol name that is deprecated.
+        new_load: {type}`str` the new load location under `//`.
+        old_load: {type}`str` the symbol import location that we are deprecating.
+        snippet: {type}`str` the usage snippet of the new symbol.
+
+    Returns:
+        The kwargs to be used in the macro creation.
+    """
+
+    if config.enable_deprecation_warnings:
+        deprecation = _DEPRECATION_MESSAGE.format(
+            name = symbol_name,
+            old_load = old_load,
+            new_load = new_load,
+            snippet = snippet,
+        )
+        if kwargs.get("deprecation"):
+            deprecation = kwargs.get("deprecation") + "\n\n" + deprecation
+        kwargs["deprecation"] = deprecation
+    return kwargs
+
+with_deprecation = struct(
+    symbol = _symbol,
+)
diff --git a/python/private/internal_config_repo.bzl b/python/private/internal_config_repo.bzl
index 7b6869e9a5..a5c4787161 100644
--- a/python/private/internal_config_repo.bzl
+++ b/python/private/internal_config_repo.bzl
@@ -18,12 +18,17 @@ such as globals available to Bazel versions, or propagating user environment
 settings for rules to later use.
 """
 
+load(":repo_utils.bzl", "repo_utils")
+
 _ENABLE_PYSTAR_ENVVAR_NAME = "RULES_PYTHON_ENABLE_PYSTAR"
 _ENABLE_PYSTAR_DEFAULT = "1"
+_ENABLE_DEPRECATION_WARNINGS_ENVVAR_NAME = "RULES_PYTHON_DEPRECATION_WARNINGS"
+_ENABLE_DEPRECATION_WARNINGS_DEFAULT = "0"
 
 _CONFIG_TEMPLATE = """\
 config = struct(
   enable_pystar = {enable_pystar},
+  enable_deprecation_warnings = {enable_deprecation_warnings},
   BuiltinPyInfo = getattr(getattr(native, "legacy_globals", None), "PyInfo", {builtin_py_info_symbol}),
   BuiltinPyRuntimeInfo = getattr(getattr(native, "legacy_globals", None), "PyRuntimeInfo", {builtin_py_runtime_info_symbol}),
   BuiltinPyCcLinkParamsProvider = getattr(getattr(native, "legacy_globals", None), "PyCcLinkParamsProvider", {builtin_py_cc_link_params_provider}),
@@ -79,6 +84,7 @@ def _internal_config_repo_impl(rctx):
 
     rctx.file("rules_python_config.bzl", _CONFIG_TEMPLATE.format(
         enable_pystar = enable_pystar,
+        enable_deprecation_warnings = _bool_from_environ(rctx, _ENABLE_DEPRECATION_WARNINGS_ENVVAR_NAME, _ENABLE_DEPRECATION_WARNINGS_DEFAULT),
         builtin_py_info_symbol = builtin_py_info_symbol,
         builtin_py_runtime_info_symbol = builtin_py_runtime_info_symbol,
         builtin_py_cc_link_params_provider = builtin_py_cc_link_params_provider,
@@ -112,4 +118,4 @@ internal_config_repo = repository_rule(
 )
 
 def _bool_from_environ(rctx, key, default):
-    return bool(int(rctx.os.environ.get(key, default)))
+    return bool(int(repo_utils.getenv(rctx, key, default)))
diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl
index 7e9a0c7ff9..5082047135 100644
--- a/python/private/toolchains_repo.bzl
+++ b/python/private/toolchains_repo.bzl
@@ -151,47 +151,39 @@ toolchain_aliases(
     rctx.file("defs.bzl", content = """\
 # Generated by python/private/toolchains_repo.bzl
 
-load(
-    "{rules_python}//python/config_settings:transition.bzl",
-    _py_binary = "py_binary",
-    _py_test = "py_test",
-)
+load("{rules_python}//python:pip.bzl", _compile_pip_requirements = "compile_pip_requirements")
+load("{rules_python}//python/private:deprecation.bzl", "with_deprecation")
+load("{rules_python}//python/private:text_util.bzl", "render")
+load("{rules_python}//python:py_binary.bzl", _py_binary = "py_binary")
+load("{rules_python}//python:py_test.bzl", _py_test = "py_test")
 load(
     "{rules_python}//python/entry_points:py_console_script_binary.bzl",
     _py_console_script_binary = "py_console_script_binary",
 )
-load("{rules_python}//python:pip.bzl", _compile_pip_requirements = "compile_pip_requirements")
 
-def py_binary(name, **kwargs):
-    return _py_binary(
-        name = name,
-        python_version = "{python_version}",
-        **kwargs
+def _with_deprecation(kwargs, *, name):
+    kwargs["python_version"] = "{python_version}"
+    return with_deprecation.symbol(
+        kwargs,
+        symbol_name = name,
+        old_load = "@{name}//:defs.bzl",
+        new_load = "@rules_python//python:{{}}.bzl".format(name),
+        snippet = render.call(name, **{{k: repr(v) for k,v in kwargs.items()}})
     )
 
-def py_console_script_binary(name, **kwargs):
-    return _py_console_script_binary(
-        name = name,
-        binary_rule = py_binary,
-        **kwargs
-    )
+def py_binary(**kwargs):
+    return _py_binary(**_with_deprecation(kwargs, name = "py_binary"))
 
-def py_test(name, **kwargs):
-    return _py_test(
-        name = name,
-        python_version = "{python_version}",
-        **kwargs
-    )
+def py_console_script_binary(**kwargs):
+    return _py_console_script_binary(**_with_deprecation(kwargs, name = "py_console_script_binary"))
 
-def compile_pip_requirements(name, **kwargs):
-    return _compile_pip_requirements(
-        name = name,
-        py_binary = py_binary,
-        py_test = py_test,
-        **kwargs
-    )
+def py_test(**kwargs):
+    return _py_test(**_with_deprecation(kwargs, name = "py_test"))
 
+def compile_pip_requirements(**kwargs):
+    return _compile_pip_requirements(**_with_deprecation(kwargs, name = "compile_pip_requirements"))
 """.format(
+        name = rctx.attr.name,
         python_version = rctx.attr.python_version,
         rules_python = get_repository_name(rctx.attr._rules_python_workspace),
     ))
@@ -316,20 +308,42 @@ def _multi_toolchain_aliases_impl(rctx):
         rctx.file(file, content = """\
 # Generated by python/private/toolchains_repo.bzl
 
+load("{rules_python}//python:pip.bzl", _compile_pip_requirements = "compile_pip_requirements")
+load("{rules_python}//python/private:deprecation.bzl", "with_deprecation")
+load("{rules_python}//python/private:text_util.bzl", "render")
+load("{rules_python}//python:py_binary.bzl", _py_binary = "py_binary")
+load("{rules_python}//python:py_test.bzl", _py_test = "py_test")
 load(
-    "@{repository_name}//:defs.bzl",
-    _compile_pip_requirements = "compile_pip_requirements",
-    _py_binary = "py_binary",
+    "{rules_python}//python/entry_points:py_console_script_binary.bzl",
     _py_console_script_binary = "py_console_script_binary",
-    _py_test = "py_test",
 )
 
-compile_pip_requirements = _compile_pip_requirements
-py_binary = _py_binary
-py_console_script_binary = _py_console_script_binary
-py_test = _py_test
+def _with_deprecation(kwargs, *, name):
+    kwargs["python_version"] = "{python_version}"
+    return with_deprecation.symbol(
+        kwargs,
+        symbol_name = name,
+        old_load = "@{name}//{python_version}:defs.bzl",
+        new_load = "@rules_python//python:{{}}.bzl".format(name),
+        snippet = render.call(name, **{{k: repr(v) for k,v in kwargs.items()}})
+    )
+
+def py_binary(**kwargs):
+    return _py_binary(**_with_deprecation(kwargs, name = "py_binary"))
+
+def py_console_script_binary(**kwargs):
+    return _py_console_script_binary(**_with_deprecation(kwargs, name = "py_console_script_binary"))
+
+def py_test(**kwargs):
+    return _py_test(**_with_deprecation(kwargs, name = "py_test"))
+
+def compile_pip_requirements(**kwargs):
+    return _compile_pip_requirements(**_with_deprecation(kwargs, name = "compile_pip_requirements"))
 """.format(
             repository_name = repository_name,
+            name = rctx.attr.name,
+            python_version = python_version,
+            rules_python = get_repository_name(rctx.attr._rules_python_workspace),
         ))
         rctx.file("{}/BUILD.bazel".format(python_version), "")
 
diff --git a/python/uv/private/lock.bzl b/python/uv/private/lock.bzl
index 217b6e4831..f4dfa36eff 100644
--- a/python/uv/private/lock.bzl
+++ b/python/uv/private/lock.bzl
@@ -17,7 +17,6 @@
 
 load("@bazel_skylib//rules:write_file.bzl", "write_file")
 load("//python:py_binary.bzl", "py_binary")
-load("//python/config_settings:transition.bzl", transition_py_binary = "py_binary")
 load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")  # buildifier: disable=bzl-visibility
 
 visibility(["//..."])
@@ -94,7 +93,7 @@ def lock(*, name, srcs, out, upgrade = False, universal = True, python_version =
         ],
     )
     if python_version:
-        py_binary_rule = lambda *args, **kwargs: transition_py_binary(python_version = python_version, *args, **kwargs)
+        py_binary_rule = lambda *args, **kwargs: py_binary(python_version = python_version, *args, **kwargs)
     else:
         py_binary_rule = py_binary
 
diff --git a/tests/config_settings/transition/multi_version_tests.bzl b/tests/config_settings/transition/multi_version_tests.bzl
index 50b4402fce..aca341a295 100644
--- a/tests/config_settings/transition/multi_version_tests.bzl
+++ b/tests/config_settings/transition/multi_version_tests.bzl
@@ -16,8 +16,9 @@
 load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
 load("@rules_testing//lib:test_suite.bzl", "test_suite")
 load("@rules_testing//lib:util.bzl", "TestingAspectInfo", rt_util = "util")
+load("//python:py_binary.bzl", "py_binary")
 load("//python:py_info.bzl", "PyInfo")
-load("//python/config_settings:transition.bzl", py_binary_transitioned = "py_binary", py_test_transitioned = "py_test")
+load("//python:py_test.bzl", "py_test")
 load("//python/private:reexports.bzl", "BuiltinPyInfo")  # buildifier: disable=bzl-visibility
 load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER")  # buildifier: disable=bzl-visibility
 load("//tests/support:support.bzl", "CC_TOOLCHAIN")
@@ -34,7 +35,7 @@ _tests = []
 
 def _test_py_test_with_transition(name):
     rt_util.helper_target(
-        py_test_transitioned,
+        py_test,
         name = name + "_subject",
         srcs = [name + "_subject.py"],
         python_version = _PYTHON_VERSION,
@@ -56,7 +57,7 @@ _tests.append(_test_py_test_with_transition)
 
 def _test_py_binary_with_transition(name):
     rt_util.helper_target(
-        py_binary_transitioned,
+        py_binary,
         name = name + "_subject",
         srcs = [name + "_subject.py"],
         python_version = _PYTHON_VERSION,
@@ -78,7 +79,7 @@ _tests.append(_test_py_binary_with_transition)
 
 def _setup_py_binary_windows(name, *, impl, build_python_zip):
     rt_util.helper_target(
-        py_binary_transitioned,
+        py_binary,
         name = name + "_subject",
         srcs = [name + "_subject.py"],
         python_version = _PYTHON_VERSION,
diff --git a/tests/deprecated/BUILD.bazel b/tests/deprecated/BUILD.bazel
new file mode 100644
index 0000000000..4b920679f1
--- /dev/null
+++ b/tests/deprecated/BUILD.bazel
@@ -0,0 +1,96 @@
+load("@bazel_skylib//rules:build_test.bzl", "build_test")
+load(
+    "@python//3.11:defs.bzl",
+    hub_compile_pip_requirements = "compile_pip_requirements",
+    hub_py_binary = "py_binary",
+    hub_py_console_script_binary = "py_console_script_binary",
+    hub_py_test = "py_test",
+)
+load(
+    "@python_3_11//:defs.bzl",
+    versioned_compile_pip_requirements = "compile_pip_requirements",
+    versioned_py_binary = "py_binary",
+    versioned_py_console_script_binary = "py_console_script_binary",
+    versioned_py_test = "py_test",
+)
+load("//python/config_settings:transition.bzl", transition_py_binary = "py_binary", transition_py_test = "py_test")
+
+# TODO @aignas 2025-01-22: remove the referenced symbols when releasing v2
+
+transition_py_binary(
+    name = "transition_py_binary",
+    srcs = ["dummy.py"],
+    main = "dummy.py",
+    python_version = "3.11",
+)
+
+transition_py_test(
+    name = "transition_py_test",
+    srcs = ["dummy.py"],
+    main = "dummy.py",
+    python_version = "3.11",
+)
+
+versioned_py_binary(
+    name = "versioned_py_binary",
+    srcs = ["dummy.py"],
+    main = "dummy.py",
+)
+
+versioned_py_test(
+    name = "versioned_py_test",
+    srcs = ["dummy.py"],
+    main = "dummy.py",
+)
+
+versioned_py_console_script_binary(
+    name = "versioned_py_console_script_binary",
+    pkg = "@rules_python_publish_deps//twine",
+    script = "twine",
+)
+
+versioned_compile_pip_requirements(
+    name = "versioned_compile_pip_requirements",
+    src = "requirements.in",
+    requirements_txt = "requirements.txt",
+)
+
+hub_py_binary(
+    name = "hub_py_binary",
+    srcs = ["dummy.py"],
+    main = "dummy.py",
+)
+
+hub_py_test(
+    name = "hub_py_test",
+    srcs = ["dummy.py"],
+    main = "dummy.py",
+)
+
+hub_py_console_script_binary(
+    name = "hub_py_console_script_binary",
+    pkg = "@rules_python_publish_deps//twine",
+    script = "twine",
+)
+
+hub_compile_pip_requirements(
+    name = "hub_compile_pip_requirements",
+    src = "requirements.in",
+    requirements_txt = "requirements_hub.txt",
+)
+
+build_test(
+    name = "build_test",
+    targets = [
+        "transition_py_binary",
+        "transition_py_test",
+        "versioned_py_binary",
+        "versioned_py_test",
+        "versioned_py_console_script_binary",
+        "versioned_compile_pip_requirements",
+        "hub_py_binary",
+        "hub_py_test",
+        "hub_py_console_script_binary",
+        "hub_compile_pip_requirements",
+    ],
+)
diff --git a/tests/deprecated/dummy.py b/tests/deprecated/dummy.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/deprecated/requirements.in b/tests/deprecated/requirements.in
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/deprecated/requirements.txt b/tests/deprecated/requirements.txt
new file mode 100644
index 0000000000..4d53f7c4e3
--- /dev/null
+++ b/tests/deprecated/requirements.txt
@@ -0,0 +1,6 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+#    bazel run //tests/deprecated:versioned_compile_pip_requirements.update
+#
diff --git a/tests/deprecated/requirements_hub.txt b/tests/deprecated/requirements_hub.txt
new file mode 100644
index 0000000000..444beb63a5
--- /dev/null
+++ b/tests/deprecated/requirements_hub.txt
@@ -0,0 +1,6 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+#    bazel run //tests/deprecated:hub_compile_pip_requirements.update
+#
diff --git a/tools/publish/BUILD.bazel b/tools/publish/BUILD.bazel
index 1648ac85df..4cf99e4d97 100644
--- a/tools/publish/BUILD.bazel
+++ b/tools/publish/BUILD.bazel
@@ -1,14 +1,10 @@
-load("//python/config_settings:transition.bzl", "py_binary")
 load("//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary")
 load("//tools/private:publish_deps.bzl", "publish_deps")
 
 py_console_script_binary(
     name = "twine",
-    # We use a py_binary rule with version transitions to ensure that we do not
-    # rely on the default version of the registered python toolchain. What is more
-    # we are using this instead of `@python_versions//3.11:defs.bzl` because loading
-    # that file relies on bzlmod being enabled.
-    binary_rule = py_binary,
+    # We transition to a specific python version in order to ensure that we
+    # don't rely on the default version configured by the root module.
     pkg = "@rules_python_publish_deps//twine",
     python_version = "3.11",
     script = "twine",