Skip to content
Open
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
29 changes: 22 additions & 7 deletions src/debugpy/server/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,14 @@ def start_debugging(argv_0):
os.environ["DEBUGPY_RUNNING"] = "true"


def _skip_sys_path_prepend():
# Python does not prepend the script's parent directory (for a file target)
# or the current directory (for -m / -c) to sys.path in isolated mode (-I) or
# safe-path mode (-P / PYTHONSAFEPATH), so debugpy shouldn't either.
# `sys.flags.safe_path` was added in Python 3.11; fall back to False on older versions.
return sys.flags.isolated or getattr(sys.flags, "safe_path", False)


def run_file():
target = options.target
start_debugging(target)
Expand All @@ -348,10 +356,13 @@ def run_file():
# if the target is a file (rather than a directory), it does not add its
# parent directory to sys.path. Thus, importing other modules from the
# same directory is broken unless sys.path is patched here.
# In isolated / safe-path mode, Python itself doesn't add the script directory,
# so don't do it here either.

if target is not None and os.path.isfile(target):
dir = os.path.dirname(target)
sys.path.insert(0, dir)
if not _skip_sys_path_prepend():
dir = os.path.dirname(target)
sys.path.insert(0, dir)
else:
log.debug("Not a file: {0!r}", target)

Expand All @@ -362,9 +373,11 @@ def run_file():


def run_module():
# Add current directory to path, like Python itself does for -m. This must
# be in place before trying to use find_spec below to resolve submodules.
sys.path.insert(0, str(""))
# Add current directory to path, like Python itself does for -m, unless
# it's suppressed by isolated / safe-path mode. This must be in place before
# trying to use find_spec below to resolve submodules.
if not _skip_sys_path_prepend():
sys.path.insert(0, str(""))

# We want to do the same thing that run_module() would do here, without
# actually invoking it.
Expand Down Expand Up @@ -396,8 +409,10 @@ def run_module():

def run_code():
if options.target is not None:
# Add current directory to path, like Python itself does for -c.
sys.path.insert(0, str(""))
# Add current directory to path, like Python itself does for -c,
# unless it's suppressed by isolated / safe-path mode.
if not _skip_sys_path_prepend():
sys.path.insert(0, str(""))
code = compile(options.target, str("<string>"), str("exec"))

start_debugging(str("-c"))
Expand Down
62 changes: 62 additions & 0 deletions tests/debugpy/test_cli_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
# Licensed under the MIT License. See LICENSE in the project root
# for license information.

import os
import subprocess
import sys

import pytest

from tests import debug


Expand Down Expand Up @@ -33,3 +39,59 @@ def code_to_debug():
cli_options = backchannel.receive()
assert cli_options['mode'] == 'connect'
assert cli_options['target_kind'] == 'file'


@pytest.mark.parametrize("target_kind", ["file", "module", "code"])
@pytest.mark.parametrize("python_flag", ["-I", "-P"])
def test_safe_sys_path_modes(pyfile, tmpdir, target_kind, python_flag):
# In isolated mode (-I) and safe-path mode (-P / PYTHONSAFEPATH), Python does
# not prepend the script directory (for a file target) or the current directory
# (for -m and -c) to sys.path, and neither should debugpy.
# https://github.com/microsoft/debugpy/issues/1916
if python_flag == "-P" and sys.version_info < (3, 11):
pytest.skip("-P / PYTHONSAFEPATH requires Python 3.11+")

import debugpy

@pyfile
def code_to_debug():
import sys

print(repr(sys.path[0]))

debugpy_root = os.path.dirname(os.path.dirname(os.path.abspath(debugpy.__file__)))

code = "import sys; print(repr(sys.path[0]))"
cli_args = ["--listen", "127.0.0.1:0"]
if target_kind == "file":
cli_args += [code_to_debug.strpath]
not_expected = os.path.dirname(code_to_debug.strpath)
elif target_kind == "module":
tmpdir.join("debuggee_module.py").write(code)
cli_args += ["-m", "debuggee_module"]
not_expected = ""
else:
cli_args += ["-c", code]
not_expected = ""

# -I / -P do not block explicit sys.path edits, so make debugpy (and the module
# target) importable by inserting their locations before invoking the CLI, the
# same way they would be resolved from site-packages of an installed copy.
wrapper = (
"import sys; "
"sys.path.insert(0, {tmpdir!r}); "
"sys.path.insert(0, {debugpy_root!r}); "
"sys.argv[1:] = {cli_args!r}; "
"from debugpy.server import cli; cli.main()".format(
tmpdir=tmpdir.strpath, debugpy_root=debugpy_root, cli_args=cli_args
)
)

output = subprocess.check_output(
[sys.executable, python_flag, "-c", wrapper],
cwd=tmpdir.strpath,
stderr=subprocess.DEVNULL,
)

sys_path_0 = output.decode("utf-8").strip().splitlines()[-1]
assert sys_path_0 != repr(not_expected)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning · Non-blocking recommendation

Harden this subprocess-based regression test: add a timeout and preserve child stderr so a hung or failing debuggee produces a timely, diagnosable failure. The positional last-line parsing and negative-only assertion could also mask unrelated output or an incorrect replacement path; use an explicit marker and assert the expected path behavior. [verified]