Skip to content

Commit c6fde99

Browse files
shayanhoshyariShayan Hoshyarigemini-code-assist[bot]
authored
docs (debugger): Update using debuggers how to guide on using debugpy (e.g. vscode) (#3547)
Thanks to help from @rickeylev in the discussion in #3481 I was able to do a debugger integration running bazel test or bazel run straight in vscode (since it uses debugpy I suspect PyCharm should also work). Added this workflow to the docs, hoping others might also find it useful. Added page: https://rules-python--3547.org.readthedocs.build/en/3547/howto/debuggers.html Related to #3485 --------- Co-authored-by: Shayan Hoshyari <hoshyari@adobe.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 979bc93 commit c6fde99

File tree

2 files changed

+147
-5
lines changed

2 files changed

+147
-5
lines changed

docs/environment-variables.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ would be:
1111
python -Xaaa /path/to/file.py
1212
```
1313

14-
This feature is likely to be useful for the integration of debuggers. For example,
14+
This feature is useful for the integration of debuggers. For example,
1515
it would be possible to configure `RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` to
1616
be set to `/path/to/debugger.py --port 12344 --file`, resulting
1717
in the command executed being:
@@ -22,6 +22,8 @@ python /path/to/debugger.py --port 12345 --file /path/to/file.py
2222

2323
:::{seealso}
2424
The {bzl:obj}`interpreter_args` attribute.
25+
26+
The guide on {any}`How to integrate a debugger`
2527
:::
2628

2729
:::{versionadded} 1.3.0

docs/howto/debuggers.md

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33

44
# How to integrate a debugger
55

6-
This guide explains how to use the {obj}`--debugger` flag to integrate a debugger
6+
This guide explains how to integrate a debugger
77
with your Python applications built with `rules_python`.
88

9-
## Basic Usage
9+
There are two ways available: the {obj}`--debugger` flag, and the {any}`RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` environment variable.
10+
11+
## {obj}`--debugger` flag
12+
13+
### Basic Usage
1014

1115
The {obj}`--debugger` flag allows you to inject an extra dependency into `py_test`
1216
and `py_binary` targets so that they have a custom debugger available at
@@ -32,7 +36,7 @@ The specified target must be in the requirements.txt file used with
3236
`pip.parse()` to make it available to Bazel.
3337
:::
3438

35-
## Python `PYTHONBREAKPOINT` Environment Variable
39+
### Python `PYTHONBREAKPOINT` Environment Variable
3640

3741
For more fine-grained control over debugging, especially for programmatic breakpoints,
3842
you can leverage the Python built-in `breakpoint()` function and the
@@ -52,7 +56,7 @@ PYTHONBREAKPOINT=pudb.set_trace bazel run \
5256

5357
For more details on `PYTHONBREAKPOINT`, refer to the [Python documentation](https://docs.python.org/3/library/functions.html#breakpoint).
5458

55-
## Setting a default debugger
59+
### Setting a default debugger
5660

5761
By adding settings to your user or project `.bazelrc` files, you can have
5862
these settings automatically added to your bazel invocations. e.g.
@@ -64,3 +68,139 @@ common --test_env=PYTHONBREAKPOINT=pudb.set_trace
6468

6569
Note that `--test_env` isn't strictly necessary. The `py_test` and `py_binary`
6670
rules will respect the `PYTHONBREAKPOINT` environment variable in your shell.
71+
72+
## debugpy (e.g. vscode)
73+
74+
You can integrate `debugpy` (i.e. the debugger used in vscode or PyCharm) by using a launcher script. This method leverages {any}`RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` to inject the debugger into the Bazel-managed Python process.
75+
76+
For the remainder of this document, we assume you are using vscode.
77+
78+
![VS Code debugpy demo](https://raw.githubusercontent.com/shayanhoshyari/issue-reports/refs/heads/main/rules_python/vscode_debugger/docs/demo.gif)
79+
80+
81+
1. **Create a launcher script**: Save the following Python script as `.vscode/debugpy/launch.py` (or another location, adjusting `launch.json` accordingly). This script bridges VS Code's debugger with Bazel.
82+
83+
<details>
84+
<summary><code>launch.py</code></summary>
85+
86+
```python
87+
"""
88+
Launcher script for VS Code (debugpy).
89+
90+
This script is not managed by Bazel; it is invoked by VS Code's launch.json to
91+
wrap the Bazel command, injecting the debugger into the runtime environment.
92+
"""
93+
94+
import argparse
95+
import os
96+
import shlex
97+
import subprocess
98+
import sys
99+
from typing import cast
100+
101+
def main() -> None:
102+
parser = argparse.ArgumentParser(description="Launch bazel debugpy with test or run.")
103+
parser.add_argument("mode", choices=["test", "run"], help="Choose whether to run a bazel test or run.")
104+
parser.add_argument("args", help="The bazel target to test or run (e.g., //foo:bar) and any additional args")
105+
args = parser.parse_args()
106+
107+
# Import debugpy, provided by VS Code
108+
try:
109+
# debugpy._vendored is needed for force_pydevd to perform path manipulation.
110+
import debugpy._vendored # type: ignore[import-not-found]
111+
112+
# pydev_monkey patches os and subprocess functions to handle new launched processes.
113+
from _pydev_bundle import pydev_monkey # type: ignore[import-not-found]
114+
except ImportError as exc:
115+
print(f"Error: This script must be run via VS Code's debug adapter. Details: {exc}")
116+
sys.exit(-1)
117+
118+
# Prepare arguments for the monkey-patched process.
119+
# is_exec=False ensures we don't replace the current process immediately.
120+
patched_args = cast(list[str], pydev_monkey.patch_args(["python", "dummy.py"], is_exec=False))
121+
pydev_monkey.send_process_created_message()
122+
123+
# Extract the injected arguments (skipping the dummy python executable and script).
124+
# These args invoke the pydevd entrypoint which connects back to the debugger.
125+
rules_python_interpreter_args = " ".join(patched_args[1:-1])
126+
127+
bzl_args = shlex.split(args.args)
128+
if not bzl_args:
129+
print("Error: At least one argument (the target) is required.")
130+
sys.exit(-1)
131+
132+
cmd = [
133+
"bazel",
134+
args.mode,
135+
# Propagate environment variables to the test/run environment.
136+
"--test_env=PYDEVD_RESOLVE_SYMLINKS",
137+
"--test_env=RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS",
138+
"--test_env=IDE_PROJECT_ROOTS",
139+
bzl_args[0],
140+
]
141+
142+
if bzl_args[1:]:
143+
if args.mode == "run":
144+
# Append extra arguments for 'run' mode.
145+
cmd.append("--")
146+
cmd.extend(bzl_args[1:])
147+
elif args.mode == "test":
148+
# Append extra arguments for 'test' mode.
149+
cmd.extend([f"--test_arg={arg}" for arg in bzl_args[1:]])
150+
151+
env = {
152+
**os.environ.copy(),
153+
# Inject the debugger arguments into the rules_python toolchain.
154+
"RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS": rules_python_interpreter_args,
155+
# Ensure breakpoints hit the original source files, not Bazel's symlinks.
156+
"PYDEVD_RESOLVE_SYMLINKS": "1",
157+
}
158+
159+
# Execute Bazel.
160+
result = subprocess.run(cmd, env=env, check=False)
161+
sys.exit(result.returncode)
162+
163+
if __name__ == "__main__":
164+
main()
165+
```
166+
</details>
167+
168+
2. **Configure `launch.json`**: Add the following configurations to your `.vscode/launch.json`. This tells VS Code to use the launcher script.
169+
170+
<details>
171+
<summary><code>launch.json</code></summary>
172+
173+
```json
174+
{
175+
"version": "0.2.0",
176+
"configurations": [
177+
{
178+
"name": "Python: Bazel py run",
179+
"type": "debugpy",
180+
"request": "launch",
181+
"program": "${workspaceFolder}/.vscode/debugpy/launch.py",
182+
"args": ["run", "${input:BazelArgs}"],
183+
"console": "integratedTerminal"
184+
},
185+
{
186+
"name": "Python: Bazel py test",
187+
"type": "debugpy",
188+
"request": "launch",
189+
"program": "${workspaceFolder}/.vscode/debugpy/launch.py",
190+
"args": ["test", "${input:BazelArgs}"],
191+
"console": "integratedTerminal"
192+
}
193+
],
194+
"inputs": [
195+
{
196+
"id": "BazelArgs",
197+
"type": "promptString",
198+
"description": "Bazel target and arguments (e.g., //foo:bar --my-arg)"
199+
}
200+
]
201+
}
202+
```
203+
</details>
204+
205+
Note: If you find `justMyCode` behavior is incompatible with Bazel's symlinks (causing breakpoints to be missed), you can set `"justMyCode": false` in `launch.json` and use the `IDE_PROJECT_ROOTS` environment variable (set to `"${workspaceFolder}"`) to explicitly map your workspace.
206+

0 commit comments

Comments
 (0)