Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/debugpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"configure",
"connect",
"debug_this_thread",
"get_cli_options",
"is_client_connected",
"listen",
"log_to",
Expand Down
49 changes: 49 additions & 0 deletions src/debugpy/public_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from __future__ import annotations

import dataclasses
import functools
import typing

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


@dataclasses.dataclass(frozen=True)
class CliOptions:
"""Options that were passed to the debugpy CLI entry point."""
mode: typing.Literal["connect", "listen"]
target_kind: typing.Literal["file", "module", "code", "pid"]
address: Endpoint
log_to: str | None = None
log_to_stderr: bool = False
target: str | None = None
wait_for_client: bool = False
adapter_access_token: str | None = None
config: dict[str, object] = dataclasses.field(default_factory=dict)
parent_session_pid: int | None = None


def _api(cancelable=False):
def apply(f):
@functools.wraps(f)
Expand Down Expand Up @@ -196,4 +212,37 @@ def trace_this_thread(__should_trace: bool):
"""


def get_cli_options() -> CliOptions | None:
"""Returns the CLI options that were processed by debugpy.

These options are all the options after the CLI args and
environment variables that were processed on startup.

If the debugpy CLI entry point was not called in this process, the
returned value is None.
"""
from debugpy.server import cli

options = cli.options
if options.mode is None or options.target_kind is None or options.address is None:
# The CLI entrypoint was not called so there are no options present.
return None

# We don't return the actual options object because we don't want callers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, good idea

# to be able to mutate it. Instead we use a frozen dataclass as a snapshot
# with richer type annotations.
return CliOptions(
mode=options.mode,
target_kind=options.target_kind,
address=options.address,
log_to=options.log_to,
log_to_stderr=options.log_to_stderr,
target=options.target,
wait_for_client=options.wait_for_client,
adapter_access_token=options.adapter_access_token,
config=options.config,
parent_session_pid=options.parent_session_pid,
)


__version__: str = _version.get_versions()["version"]
10 changes: 6 additions & 4 deletions src/debugpy/server/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import re
import sys
from importlib.util import find_spec
from typing import Any, Union, Tuple, Dict
from typing import Any, Union, Tuple, Dict, Literal

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

TargetKind = Literal["file", "module", "code", "pid"]

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

Expand All @@ -42,13 +43,14 @@
)


# Changes here should be aligned with the public API CliOptions.
class Options(object):
mode = None
mode: Union[Literal["connect", "listen"], None] = None
address: Union[Tuple[str, int], None] = None
log_to = None
log_to_stderr = False
target: Union[str, None] = None
target_kind: Union[str, None] = None
target_kind: Union[TargetKind, None] = None
wait_for_client = False
adapter_access_token = None
config: Dict[str, Any] = {}
Expand Down Expand Up @@ -145,7 +147,7 @@ def set_config(arg, it):
options.config[name] = value


def set_target(kind: str, parser=(lambda x: x), positional=False):
def set_target(kind: TargetKind, parser=(lambda x: x), positional=False):
def do(arg, it):
options.target_kind = kind
target = parser(arg if positional else next(it))
Expand Down
35 changes: 35 additions & 0 deletions tests/debugpy/test_cli_args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.

from tests import debug


def test_cli_options_with_no_debugger():
import debugpy

cli_options = debugpy.get_cli_options()
assert cli_options is None


def test_cli_options_under_file_connect(pyfile, target, run):
@pyfile
def code_to_debug():
import dataclasses
import debugpy

import debuggee
from debuggee import backchannel

debuggee.setup()
backchannel.send(dataclasses.asdict(debugpy.get_cli_options()))

with debug.Session() as session:
backchannel = session.open_backchannel()

with run(session, target(code_to_debug)):
pass

cli_options = backchannel.receive()
assert cli_options['mode'] == 'connect'
assert cli_options['target_kind'] == 'file'
9 changes: 6 additions & 3 deletions tests/debugpy/test_multiproc.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ def parent():
import subprocess
import sys

from debugpy.server import cli as debugpy_cli
import debugpy

debuggee.setup()

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

host, port = debugpy_cli.options.address
access_token = debugpy_cli.options.adapter_access_token
cli_opts = debugpy.get_cli_options()
assert cli_opts, "No CLI options found"

host, port = cli_opts.address
access_token = cli_opts.adapter_access_token

shell_args = [
sys.executable,
Expand Down
Loading