Skip to content

Commit 1aff9aa

Browse files
authored
Expose CLI Options through public API (#1928)
* Expose CLI Options through public API Expose a public API that can retrieve the processed CLI options for the current process launched through the debugpy CLI. This enables code to be able to retrieve options like the port and adapter access token to be used for launching their own child process' that can be debugged. * Fix test by sending dict not dataclass object
1 parent ead90f6 commit 1aff9aa

5 files changed

Lines changed: 97 additions & 7 deletions

File tree

src/debugpy/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"configure",
1616
"connect",
1717
"debug_this_thread",
18+
"get_cli_options",
1819
"is_client_connected",
1920
"listen",
2021
"log_to",

src/debugpy/public_api.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from __future__ import annotations
66

7+
import dataclasses
78
import functools
89
import typing
910

@@ -21,6 +22,21 @@
2122
Endpoint = typing.Tuple[str, int]
2223

2324

25+
@dataclasses.dataclass(frozen=True)
26+
class CliOptions:
27+
"""Options that were passed to the debugpy CLI entry point."""
28+
mode: typing.Literal["connect", "listen"]
29+
target_kind: typing.Literal["file", "module", "code", "pid"]
30+
address: Endpoint
31+
log_to: str | None = None
32+
log_to_stderr: bool = False
33+
target: str | None = None
34+
wait_for_client: bool = False
35+
adapter_access_token: str | None = None
36+
config: dict[str, object] = dataclasses.field(default_factory=dict)
37+
parent_session_pid: int | None = None
38+
39+
2440
def _api(cancelable=False):
2541
def apply(f):
2642
@functools.wraps(f)
@@ -196,4 +212,37 @@ def trace_this_thread(__should_trace: bool):
196212
"""
197213

198214

215+
def get_cli_options() -> CliOptions | None:
216+
"""Returns the CLI options that were processed by debugpy.
217+
218+
These options are all the options after the CLI args and
219+
environment variables that were processed on startup.
220+
221+
If the debugpy CLI entry point was not called in this process, the
222+
returned value is None.
223+
"""
224+
from debugpy.server import cli
225+
226+
options = cli.options
227+
if options.mode is None or options.target_kind is None or options.address is None:
228+
# The CLI entrypoint was not called so there are no options present.
229+
return None
230+
231+
# We don't return the actual options object because we don't want callers
232+
# to be able to mutate it. Instead we use a frozen dataclass as a snapshot
233+
# with richer type annotations.
234+
return CliOptions(
235+
mode=options.mode,
236+
target_kind=options.target_kind,
237+
address=options.address,
238+
log_to=options.log_to,
239+
log_to_stderr=options.log_to_stderr,
240+
target=options.target,
241+
wait_for_client=options.wait_for_client,
242+
adapter_access_token=options.adapter_access_token,
243+
config=options.config,
244+
parent_session_pid=options.parent_session_pid,
245+
)
246+
247+
199248
__version__: str = _version.get_versions()["version"]

src/debugpy/server/cli.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import re
88
import sys
99
from importlib.util import find_spec
10-
from typing import Any, Union, Tuple, Dict
10+
from typing import Any, Union, Tuple, Dict, Literal
1111

1212
# debugpy.__main__ should have preloaded pydevd properly before importing this module.
1313
# Otherwise, some stdlib modules above might have had imported threading before pydevd
@@ -23,6 +23,7 @@
2323
from debugpy.common import log, sockets
2424
from debugpy.server import api
2525

26+
TargetKind = Literal["file", "module", "code", "pid"]
2627

2728
TARGET = "<filename> | -m <module> | -c <code> | --pid <pid>"
2829

@@ -43,13 +44,14 @@
4344
)
4445

4546

47+
# Changes here should be aligned with the public API CliOptions.
4648
class Options(object):
47-
mode = None
49+
mode: Union[Literal["connect", "listen"], None] = None
4850
address: Union[Tuple[str, int], None] = None
4951
log_to = None
5052
log_to_stderr = False
5153
target: Union[str, None] = None
52-
target_kind: Union[str, None] = None
54+
target_kind: Union[TargetKind, None] = None
5355
wait_for_client = False
5456
adapter_access_token = None
5557
config: Dict[str, Any] = {}
@@ -146,7 +148,7 @@ def set_config(arg, it):
146148
options.config[name] = value
147149

148150

149-
def set_target(kind: str, parser=(lambda x: x), positional=False):
151+
def set_target(kind: TargetKind, parser=(lambda x: x), positional=False):
150152
def do(arg, it):
151153
options.target_kind = kind
152154
target = parser(arg if positional else next(it))

tests/debugpy/test_cli_args.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License. See LICENSE in the project root
3+
# for license information.
4+
5+
from tests import debug
6+
7+
8+
def test_cli_options_with_no_debugger():
9+
import debugpy
10+
11+
cli_options = debugpy.get_cli_options()
12+
assert cli_options is None
13+
14+
15+
def test_cli_options_under_file_connect(pyfile, target, run):
16+
@pyfile
17+
def code_to_debug():
18+
import dataclasses
19+
import debugpy
20+
21+
import debuggee
22+
from debuggee import backchannel
23+
24+
debuggee.setup()
25+
backchannel.send(dataclasses.asdict(debugpy.get_cli_options()))
26+
27+
with debug.Session() as session:
28+
backchannel = session.open_backchannel()
29+
30+
with run(session, target(code_to_debug)):
31+
pass
32+
33+
cli_options = backchannel.receive()
34+
assert cli_options['mode'] == 'connect'
35+
assert cli_options['target_kind'] == 'file'

tests/debugpy/test_multiproc.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ def parent():
630630
import subprocess
631631
import sys
632632

633-
from debugpy.server import cli as debugpy_cli
633+
import debugpy
634634

635635
debuggee.setup()
636636

@@ -642,8 +642,11 @@ def parent():
642642
else:
643643
argv = ["/bin/sh", "-c"]
644644

645-
host, port = debugpy_cli.options.address
646-
access_token = debugpy_cli.options.adapter_access_token
645+
cli_opts = debugpy.get_cli_options()
646+
assert cli_opts, "No CLI options found"
647+
648+
host, port = cli_opts.address
649+
access_token = cli_opts.adapter_access_token
647650

648651
shell_args = [
649652
sys.executable,

0 commit comments

Comments
 (0)