Skip to content

Commit a94bd0f

Browse files
feat: add --debugger flag (#3478)
The --debugger flag is useful for injecting a user-specified dependency without having to modify the binary or test. Similarly, tests now implicitly inherit the `PYTHONBREAKPOINT` environment variable. The dependency is only added for the target config because build tools can't be intercepted for debugging. --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent da822a8 commit a94bd0f

File tree

12 files changed

+198
-11
lines changed

12 files changed

+198
-11
lines changed

CHANGELOG.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,27 @@ BEGIN_UNRELEASED_TEMPLATE
4747
END_UNRELEASED_TEMPLATE
4848
-->
4949

50+
{#v0-0-0}
51+
## Unreleased
52+
53+
[0.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.0.0
54+
55+
{#v0-0-0-removed}
56+
### Removed
57+
* Nothing removed.
58+
59+
{#v0-0-0-changed}
60+
### Changed
61+
* (binaries/tests) The `PYTHONBREAKPOINT` environment variable is automatically inherited
62+
63+
{#v0-0-0-fixed}
64+
### Fixed
65+
* Nothing fixed.
66+
67+
{#v0-0-0-added}
68+
### Added
69+
* (binaries/tests) {obj}`--debugger`: allows specifying an extra dependency
70+
to add to binaries/tests for custom debuggers.
5071

5172
{#v1-8-0}
5273
## [1.8.0] - 2025-12-19
@@ -2065,4 +2086,4 @@ Breaking changes:
20652086
* (pip) Create all_data_requirements alias
20662087
* Expose Python C headers through the toolchain.
20672088

2068-
[0.24.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.24.0
2089+
[0.24.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.24.0

docs/api/rules_python/python/config_settings/index.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,27 @@ This flag replaces the Bazel builtin `--build_python_zip` flag.
4545
:::
4646
::::
4747

48+
::::{bzl:flag} debugger
49+
A target for providing a custom debugger dependency.
50+
51+
This flag is roughly equivalent to putting a target in `deps`. It allows
52+
injecting a dependency into executables (`py_binary`, `py_test`) without having
53+
to modify their deps. The expectation is it points to a target that provides an
54+
alternative debugger (pudb, winpdb, debugpy, etc).
55+
56+
* Must provide {obj}`PyInfo`.
57+
* This dependency is only used for the target config, i.e. build tools don't
58+
have it added.
59+
60+
:::{note}
61+
Setting this flag adds the debugger dependency, but doesn't automatically set
62+
`PYTHONBREAKPOINT` to change `breakpoint()` behavior.
63+
:::
64+
65+
:::{versionadded} VERSION_NEXT_FEATURE
66+
:::
67+
::::
68+
4869
::::{bzl:flag} experimental_python_import_all_repositories
4970
Controls whether repository directories are added to the import path.
5071

docs/howto/debuggers.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
:::{default-domain} bzl
2+
:::
3+
4+
# How to integrate a debugger
5+
6+
This guide explains how to use the {obj}`--debugger` flag to integrate a debugger
7+
with your Python applications built with `rules_python`.
8+
9+
## Basic Usage
10+
11+
The {obj}`--debugger` flag allows you to inject an extra dependency into `py_test`
12+
and `py_binary` targets so that they have a custom debugger available at
13+
runtime. The flag is roughly equivalent to manually adding it to `deps` of
14+
the target under test.
15+
16+
To use the debugger, you typically provide the `--debugger` flag to your `bazel run` command.
17+
18+
Example command line:
19+
20+
```bash
21+
bazel run --@rules_python//python/config_settings:debugger=@pypi//pudb \
22+
//path/to:my_python_binary
23+
```
24+
25+
This will launch the Python program with the `@pypi//pudb` dependency added.
26+
27+
The exact behavior (e.g., waiting for attachment, breaking at the first line)
28+
depends on the specific debugger and its configuration.
29+
30+
:::{note}
31+
The specified target must be in the requirements.txt file used with
32+
`pip.parse()` to make it available to Bazel.
33+
:::
34+
35+
## Python `PYTHONBREAKPOINT` Environment Variable
36+
37+
For more fine-grained control over debugging, especially for programmatic breakpoints,
38+
you can leverage the Python built-in `breakpoint()` function and the
39+
`PYTHONBREAKPOINT` environment variable.
40+
41+
The `breakpoint()` built-in function, available since Python 3.7,
42+
can be called anywhere in your code to invoke a debugger. The `PYTHONBREAKPOINT`
43+
environment variable can be set to specify which debugger to use.
44+
45+
For example, to use `pdb` (the Python Debugger) when `breakpoint()` is called:
46+
47+
```bash
48+
PYTHONBREAKPOINT=pudb.set_trace bazel run \
49+
--@rules_python//python/config_settings:debugger=@pypi//pudb \
50+
//path/to:my_python_binary
51+
```
52+
53+
For more details on `PYTHONBREAKPOINT`, refer to the [Python documentation](https://docs.python.org/3/library/functions.html#breakpoint).
54+
55+
## Setting a default debugger
56+
57+
By adding settings to your user or project `.bazelrc` files, you can have
58+
these settings automatically added to your bazel invocations. e.g.
59+
60+
```
61+
common --@rules_python//python/config_settings:debugger=@pypi//pudb
62+
common --test_env=PYTHONBREAKPOINT=pudb.set_trace
63+
```
64+
65+
Note that `--test_env` isn't strictly necessary. The `py_test` and `py_binary`
66+
rules will respect the `PYTHONBREAKPOINT` environment variable in your shell.

python/config_settings/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ rp_string_flag(
102102
visibility = ["//visibility:public"],
103103
)
104104

105+
label_flag(
106+
name = "debugger",
107+
build_setting_default = "//python/private:empty",
108+
visibility = ["//visibility:public"],
109+
)
110+
105111
# For some reason, @platforms//os:windows can't be directly used
106112
# in the select() for the flag. But it can be used when put behind
107113
# a config_setting().

python/private/BUILD.bazel

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
1616
load("@bazel_skylib//rules:common_settings.bzl", "bool_setting")
1717
load("//python:py_binary.bzl", "py_binary")
1818
load("//python:py_library.bzl", "py_library")
19+
load(":bazel_config_mode.bzl", "bazel_config_mode")
1920
load(":print_toolchain_checksums.bzl", "print_toolchains_checksums")
2021
load(":py_exec_tools_toolchain.bzl", "current_interpreter_executable")
2122
load(":sentinel.bzl", "sentinel")
@@ -810,6 +811,23 @@ config_setting(
810811
},
811812
)
812813

814+
config_setting(
815+
name = "is_bazel_config_mode_target",
816+
flag_values = {
817+
"//python/private:bazel_config_mode": "target",
818+
},
819+
)
820+
821+
alias(
822+
name = "debugger_if_target_config",
823+
actual = select({
824+
":is_bazel_config_mode_target": "//python/config_settings:debugger",
825+
"//conditions:default": "//python/private:empty",
826+
}),
827+
)
828+
829+
bazel_config_mode(name = "bazel_config_mode")
830+
813831
# This should only be set by analysis tests to expose additional metadata to
814832
# aid testing, so a setting instead of a flag.
815833
bool_setting(
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Flag to tell if exec or target mode is active."""
2+
3+
load(":py_internal.bzl", "py_internal")
4+
5+
def _bazel_config_mode_impl(ctx):
6+
return [config_common.FeatureFlagInfo(
7+
value = "exec" if py_internal.is_tool_configuration(ctx) else "target",
8+
)]
9+
10+
bazel_config_mode = rule(
11+
implementation = _bazel_config_mode_impl,
12+
)

python/private/common.bzl

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ PYTHON_FILE_EXTENSIONS = [
4444
def create_binary_semantics_struct(
4545
*,
4646
get_central_uncachable_version_file,
47-
get_debugger_deps,
4847
get_native_deps_dso_name,
4948
should_build_native_deps_dso,
5049
should_include_build_data):
@@ -57,8 +56,6 @@ def create_binary_semantics_struct(
5756
get_central_uncachable_version_file: Callable that returns an optional
5857
Artifact; this artifact is special: it is never cached and is a copy
5958
of `ctx.version_file`; see py_builtins.copy_without_caching
60-
get_debugger_deps: Callable that returns a list of Targets that provide
61-
custom debugger support; only called for target-configuration.
6259
get_native_deps_dso_name: Callable that returns a string, which is the
6360
basename (with extension) of the native deps DSO library.
6461
should_build_native_deps_dso: Callable that returns bool; True if
@@ -71,7 +68,6 @@ def create_binary_semantics_struct(
7168
return struct(
7269
# keep-sorted
7370
get_central_uncachable_version_file = get_central_uncachable_version_file,
74-
get_debugger_deps = get_debugger_deps,
7571
get_native_deps_dso_name = get_native_deps_dso_name,
7672
should_build_native_deps_dso = should_build_native_deps_dso,
7773
should_include_build_data = should_include_build_data,

python/private/common_labels.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ labels = struct(
88
ADD_SRCS_TO_RUNFILES = str(Label("//python/config_settings:add_srcs_to_runfiles")),
99
BOOTSTRAP_IMPL = str(Label("//python/config_settings:bootstrap_impl")),
1010
BUILD_PYTHON_ZIP = str(Label("//python/config_settings:build_python_zip")),
11+
DEBUGGER = str(Label("//python/config_settings:debugger")),
1112
EXEC_TOOLS_TOOLCHAIN = str(Label("//python/config_settings:exec_tools_toolchain")),
1213
PIP_ENV_MARKER_CONFIG = str(Label("//python/config_settings:pip_env_marker_config")),
1314
NONE = str(Label("//python:none")),

python/private/py_executable.bzl

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,10 @@ accepting arbitrary Python versions.
205205
allow_single_file = True,
206206
default = "@bazel_tools//tools/python:python_bootstrap_template.txt",
207207
),
208+
"_debugger_flag": lambda: attrb.Label(
209+
default = "//python/private:debugger_if_target_config",
210+
providers = [PyInfo],
211+
),
208212
"_launcher": lambda: attrb.Label(
209213
cfg = "target",
210214
# NOTE: This is an executable, but is only used for Windows. It
@@ -267,17 +271,12 @@ def create_binary_semantics():
267271
return create_binary_semantics_struct(
268272
# keep-sorted start
269273
get_central_uncachable_version_file = lambda ctx: None,
270-
get_debugger_deps = _get_debugger_deps,
271274
get_native_deps_dso_name = _get_native_deps_dso_name,
272275
should_build_native_deps_dso = lambda ctx: False,
273276
should_include_build_data = lambda ctx: False,
274277
# keep-sorted end
275278
)
276279

277-
def _get_debugger_deps(ctx, runtime_details):
278-
_ = ctx, runtime_details # @unused
279-
return []
280-
281280
def _should_create_init_files(ctx):
282281
if ctx.attr.legacy_create_init == -1:
283282
return not read_possibly_native_flag(ctx, "default_to_explicit_init_py")
@@ -1025,7 +1024,7 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
10251024
# The debugger dependency should be prevented by select() config elsewhere,
10261025
# but just to be safe, also guard against adding it to the output here.
10271026
if not _is_tool_config(ctx):
1028-
extra_deps.extend(semantics.get_debugger_deps(ctx, runtime_details))
1027+
extra_deps.append(ctx.attr._debugger_flag)
10291028

10301029
cc_details = _get_cc_details_for_binary(ctx, extra_deps = extra_deps)
10311030
native_deps_details = _get_native_deps_details(
@@ -1751,6 +1750,8 @@ def _create_run_environment_info(ctx, inherited_environment):
17511750
expression = value,
17521751
targets = ctx.attr.data,
17531752
)
1753+
if "PYTHONBREAKPOINT" not in inherited_environment:
1754+
inherited_environment = inherited_environment + ["PYTHONBREAKPOINT"]
17541755
return RunEnvironmentInfo(
17551756
environment = expanded_env,
17561757
inherited_environment = inherited_environment,

python/private/transition_labels.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ load(":common_labels.bzl", "labels")
1010
_BASE_TRANSITION_LABELS = [
1111
labels.ADD_SRCS_TO_RUNFILES,
1212
labels.BOOTSTRAP_IMPL,
13+
labels.DEBUGGER,
1314
labels.EXEC_TOOLS_TOOLCHAIN,
1415
labels.PIP_ENV_MARKER_CONFIG,
1516
labels.PIP_WHL_MUSLC_VERSION,

0 commit comments

Comments
 (0)