Skip to content

Commit b2665b2

Browse files
authored
Merge branch 'main' into patch-2
2 parents 053ee57 + a1ca5d4 commit b2665b2

File tree

13 files changed

+308
-191
lines changed

13 files changed

+308
-191
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: 20 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")
@@ -27,6 +28,8 @@ package(
2728

2829
licenses(["notice"])
2930

31+
exports_files(["runtime_env_toolchain_interpreter.sh"])
32+
3033
filegroup(
3134
name = "distribution",
3235
srcs = glob(["**"]) + [
@@ -810,6 +813,23 @@ config_setting(
810813
},
811814
)
812815

816+
config_setting(
817+
name = "is_bazel_config_mode_target",
818+
flag_values = {
819+
"//python/private:bazel_config_mode": "target",
820+
},
821+
)
822+
823+
alias(
824+
name = "debugger_if_target_config",
825+
actual = select({
826+
":is_bazel_config_mode_target": "//python/config_settings:debugger",
827+
"//conditions:default": "//python/private:empty",
828+
}),
829+
)
830+
831+
bazel_config_mode(name = "bazel_config_mode")
832+
813833
# This should only be set by analysis tests to expose additional metadata to
814834
# aid testing, so a setting instead of a flag.
815835
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: 22 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -43,116 +43,36 @@ PYTHON_FILE_EXTENSIONS = [
4343

4444
def create_binary_semantics_struct(
4545
*,
46-
create_executable,
47-
get_cc_details_for_binary,
4846
get_central_uncachable_version_file,
49-
get_coverage_deps,
50-
get_debugger_deps,
51-
get_extra_common_runfiles_for_binary,
52-
get_extra_providers,
53-
get_extra_write_build_data_env,
54-
get_interpreter_path,
55-
get_imports,
5647
get_native_deps_dso_name,
57-
get_native_deps_user_link_flags,
58-
get_stamp_flag,
59-
maybe_precompile,
6048
should_build_native_deps_dso,
61-
should_create_init_files,
6249
should_include_build_data):
6350
"""Helper to ensure a semantics struct has all necessary fields.
6451
6552
Call this instead of a raw call to `struct(...)`; it'll help ensure all
6653
the necessary functions are being correctly provided.
6754
6855
Args:
69-
create_executable: Callable; creates a binary's executable output. See
70-
py_executable.bzl#py_executable_base_impl for details.
71-
get_cc_details_for_binary: Callable that returns a `CcDetails` struct; see
72-
`create_cc_detail_struct`.
7356
get_central_uncachable_version_file: Callable that returns an optional
7457
Artifact; this artifact is special: it is never cached and is a copy
7558
of `ctx.version_file`; see py_builtins.copy_without_caching
76-
get_coverage_deps: Callable that returns a list of Targets for making
77-
coverage work; only called if coverage is enabled.
78-
get_debugger_deps: Callable that returns a list of Targets that provide
79-
custom debugger support; only called for target-configuration.
80-
get_extra_common_runfiles_for_binary: Callable that returns a runfiles
81-
object of extra runfiles a binary should include.
82-
get_extra_providers: Callable that returns extra providers; see
83-
py_executable.bzl#_create_providers for details.
84-
get_extra_write_build_data_env: Callable that returns a dict[str, str]
85-
of additional environment variable to pass to build data generation.
86-
get_interpreter_path: Callable that returns an optional string, which is
87-
the path to the Python interpreter to use for running the binary.
88-
get_imports: Callable that returns a list of the target's import
89-
paths (from the `imports` attribute, so just the target's own import
90-
path strings, not from dependencies).
9159
get_native_deps_dso_name: Callable that returns a string, which is the
9260
basename (with extension) of the native deps DSO library.
93-
get_native_deps_user_link_flags: Callable that returns a list of strings,
94-
which are any extra linker flags to pass onto the native deps DSO
95-
linking action.
96-
get_stamp_flag: Callable that returns bool of if the --stamp flag was
97-
enabled or not.
98-
maybe_precompile: Callable that may optional precompile the input `.py`
99-
sources and returns the full set of desired outputs derived from
100-
the source files (e.g., both py and pyc, only one of them, etc).
10161
should_build_native_deps_dso: Callable that returns bool; True if
10262
building a native deps DSO is supported, False if not.
103-
should_create_init_files: Callable that returns bool; True if
104-
`__init__.py` files should be generated, False if not.
10563
should_include_build_data: Callable that returns bool; True if
10664
build data should be generated, False if not.
10765
Returns:
10866
A "BinarySemantics" struct.
10967
"""
11068
return struct(
11169
# keep-sorted
112-
create_executable = create_executable,
113-
get_cc_details_for_binary = get_cc_details_for_binary,
11470
get_central_uncachable_version_file = get_central_uncachable_version_file,
115-
get_coverage_deps = get_coverage_deps,
116-
get_debugger_deps = get_debugger_deps,
117-
get_extra_common_runfiles_for_binary = get_extra_common_runfiles_for_binary,
118-
get_extra_providers = get_extra_providers,
119-
get_extra_write_build_data_env = get_extra_write_build_data_env,
120-
get_imports = get_imports,
121-
get_interpreter_path = get_interpreter_path,
12271
get_native_deps_dso_name = get_native_deps_dso_name,
123-
get_native_deps_user_link_flags = get_native_deps_user_link_flags,
124-
get_stamp_flag = get_stamp_flag,
125-
maybe_precompile = maybe_precompile,
12672
should_build_native_deps_dso = should_build_native_deps_dso,
127-
should_create_init_files = should_create_init_files,
12873
should_include_build_data = should_include_build_data,
12974
)
13075

131-
def create_library_semantics_struct(
132-
*,
133-
get_cc_info_for_library,
134-
get_imports,
135-
maybe_precompile):
136-
"""Create a `LibrarySemantics` struct.
137-
138-
Call this instead of a raw call to `struct(...)`; it'll help ensure all
139-
the necessary functions are being correctly provided.
140-
141-
Args:
142-
get_cc_info_for_library: Callable that returns a CcInfo for the library;
143-
see py_library_impl for arg details.
144-
get_imports: Callable; see create_binary_semantics_struct.
145-
maybe_precompile: Callable; see create_binary_semantics_struct.
146-
Returns:
147-
a `LibrarySemantics` struct.
148-
"""
149-
return struct(
150-
# keep sorted
151-
get_cc_info_for_library = get_cc_info_for_library,
152-
get_imports = get_imports,
153-
maybe_precompile = maybe_precompile,
154-
)
155-
15676
def create_cc_details_struct(
15777
*,
15878
cc_info_for_propagating,
@@ -241,12 +161,8 @@ def collect_cc_info(ctx, extra_deps = []):
241161
Returns:
242162
CcInfo provider of merged information.
243163
"""
244-
deps = ctx.attr.deps
245-
if extra_deps:
246-
deps = list(deps)
247-
deps.extend(extra_deps)
248164
cc_infos = []
249-
for dep in deps:
165+
for dep in collect_deps(ctx, extra_deps):
250166
if CcInfo in dep:
251167
cc_infos.append(dep[CcInfo])
252168

@@ -255,29 +171,28 @@ def collect_cc_info(ctx, extra_deps = []):
255171

256172
return cc_common.merge_cc_infos(cc_infos = cc_infos)
257173

258-
def collect_imports(ctx, semantics):
174+
def collect_imports(ctx, extra_deps = []):
259175
"""Collect the direct and transitive `imports` strings.
260176
261177
Args:
262178
ctx: {type}`ctx` the current target ctx
263-
semantics: semantics object for fetching direct imports.
179+
extra_deps: list of Target to also collect imports from.
264180
265181
Returns:
266182
{type}`depset[str]` of import paths
267183
"""
184+
268185
transitive = []
269-
for dep in ctx.attr.deps:
186+
for dep in collect_deps(ctx, extra_deps):
270187
if PyInfo in dep:
271188
transitive.append(dep[PyInfo].imports)
272189
if BuiltinPyInfo != None and BuiltinPyInfo in dep:
273190
transitive.append(dep[BuiltinPyInfo].imports)
274-
return depset(direct = semantics.get_imports(ctx), transitive = transitive)
191+
return depset(direct = get_imports(ctx), transitive = transitive)
275192

276193
def get_imports(ctx):
277194
"""Gets the imports from a rule's `imports` attribute.
278195
279-
See create_binary_semantics_struct for details about this function.
280-
281196
Args:
282197
ctx: Rule ctx.
283198
@@ -562,3 +477,19 @@ def runfiles_root_path(ctx, short_path):
562477
return short_path[3:]
563478
else:
564479
return "{}/{}".format(ctx.workspace_name, short_path)
480+
481+
def collect_deps(ctx, extra_deps = []):
482+
"""Collect the dependencies from the rule's context.
483+
484+
Args:
485+
ctx: rule ctx
486+
extra_deps: list of Target to also collect dependencies from.
487+
488+
Returns:
489+
list of Target
490+
"""
491+
deps = ctx.attr.deps
492+
if extra_deps:
493+
deps = list(deps)
494+
deps.extend(extra_deps)
495+
return deps

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")),

0 commit comments

Comments
 (0)