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
3 changes: 2 additions & 1 deletion src/debugpy/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@

# Do not use dirname() to walk up - this can be a relative path, e.g. ".".
sys.path[0] = sys.path[0] + "/../"
import debugpy # noqa
# Not an import statement: PEP 810 lazy imports could defer it past the del below.
__import__("debugpy")
del sys.path[0]

from debugpy.server import cli
Expand Down
19 changes: 18 additions & 1 deletion src/debugpy/_vendored/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,20 @@ def check_modules(project, match, root=None):
return unvendored, extensions


@contextlib.contextmanager
def _eager_imports():
"""A context manager under which plain imports are not deferred by PEP 810."""
get_lazy_imports = getattr(sys, "get_lazy_imports", None)
if get_lazy_imports is None or get_lazy_imports() != "all":
yield
return
sys.set_lazy_imports("normal")
try:
yield
finally:
sys.set_lazy_imports("all")


@contextlib.contextmanager
def vendored(project, root=None):
"""A context manager under which the vendored project will be imported."""
Expand All @@ -114,7 +128,10 @@ def vendored(project, root=None):
# Add the vendored project directory, so that it gets tried first.
sys.path.insert(0, root)
try:
yield root
# An import resolved after the directory is removed from sys.path would
# fail, so everything must be loaded while it is still there.
with _eager_imports():
yield root
finally:
sys.path.remove(root)

Expand Down
5 changes: 3 additions & 2 deletions src/debugpy/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
# for license information.

# "force_pydevd" must be imported first to ensure (via side effects)
# that the debugpy-vendored copy of pydevd gets used.
import debugpy._vendored.force_pydevd # noqa
# that the debugpy-vendored copy of pydevd gets used. A plain import
# statement would never run it when PEP 810 lazy imports are enabled globally.
__import__("debugpy._vendored.force_pydevd")
8 changes: 6 additions & 2 deletions src/debugpy/server/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,8 @@ def attach_to_pid():
}
setup = encode(json.dumps(setup))

# __import__ rather than an import statement, which PEP 810 lazy imports could
# defer past `del sys.path[0]`. The name is encoded because pydevd rejects quotes.
python_code = """
import codecs;
import json;
Expand All @@ -466,15 +468,17 @@ def attach_to_pid():
setup = json.loads(decode({setup}));

sys.path.insert(0, script_dir);
import attach_pid_injected;
attach_pid_injected = __import__(decode({module}));
del sys.path[0];

attach_pid_injected.attach(setup);
"""
python_code = (
python_code.replace("\r", "")
.replace("\n", "")
.format(script_dir=script_dir, setup=setup)
.format(
script_dir=script_dir, setup=setup, module=encode("attach_pid_injected")
)
)

# attempt pep 768 style code injection
Expand Down
49 changes: 49 additions & 0 deletions tests/debugpy/test_lazy_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.

import sys

import pytest

from tests import debug
from tests.debug import runners, targets
from tests.patterns import some

pytestmark = pytest.mark.skipif(
not hasattr(sys, "set_lazy_imports"),
reason="PEP 810 lazy imports require Python 3.15+",
)


@pytest.mark.parametrize("run", runners.all)
@pytest.mark.parametrize("target", targets.all_named)
def test_lazy_imports_all(pyfile, target, run):
@pyfile
def code_to_debug():
import sys

import debuggee
from debuggee import backchannel

debuggee.setup()
backchannel.send(sys.get_lazy_imports()) # @bp

with debug.Session() as session:
session.spawn_adapter.env["PYTHON_LAZY_IMPORTS"] = "all"
env = (
session.config.env
if run.request == "launch"
else session.spawn_debuggee.env
)
env["PYTHON_LAZY_IMPORTS"] = "all"

backchannel = session.open_backchannel()
with run(session, target(code_to_debug)):
session.set_breakpoints(code_to_debug, all)

session.wait_for_stop(
"breakpoint", expected_frames=[some.dap.frame(code_to_debug, line="bp")]
)
session.request_continue()
assert backchannel.receive() == "all"
Loading