Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3d7808e
Make typing even more restrictive and start fixing some more errors
rchiodo Jul 5, 2024
e528e2d
More typing
rchiodo Jul 5, 2024
622e3c6
Finish typing debupy python code
rchiodo Jul 5, 2024
50e8d1d
Rest of the API typed
rchiodo Jul 8, 2024
063bb6a
Put back wait_for_client variable
rchiodo Jul 8, 2024
5dd7fe9
Update test requirements
rchiodo Jul 8, 2024
3ee1940
Fix converter
rchiodo Jul 8, 2024
891d5df
Merge remote-tracking branch 'upstream/main' into rchiodo/type_standard
rchiodo Jul 24, 2024
931d83c
Fix linter
rchiodo Jul 24, 2024
7a1350a
Fix messaging error
rchiodo Jul 24, 2024
8ce6670
Review feedback and fixup more types
rchiodo Jul 24, 2024
4d609d2
fix request_options
rchiodo Jul 24, 2024
fddf0de
Fix debug output and put wait back the way it was
rchiodo Jul 25, 2024
c02affd
Skip importing sessions unless type checking
rchiodo Jul 25, 2024
bfd9b88
Hide types to prevent runtime issue
rchiodo Jul 25, 2024
046fe6f
Fix linter
rchiodo Jul 25, 2024
0196522
Remove inappropriate static method
rchiodo Jul 25, 2024
fac2692
Merge branch 'rchiodo/type_standard' of https://github.com/rchiodo/de…
rchiodo Jul 25, 2024
6e3bde7
Fix linux failures
rchiodo Jul 25, 2024
8b8467a
Fix type errors
rchiodo Jul 25, 2024
2ce0c69
Cannot reference socket even in types
rchiodo Jul 25, 2024
e5f1889
Merge branch 'rchiodo/type_standard' of https://github.com/rchiodo/de…
rchiodo Jul 25, 2024
9c7c06b
Fix linter again
rchiodo Jul 25, 2024
4ce8028
Merge branch 'main' of https://github.com/microsoft/debugpy into rchi…
rchiodo Jul 16, 2026
6d50fc5
Address review feedback: fix runtime bugs and add tests
rchiodo Jul 16, 2026
1478a48
Fail fast on missing server listener for debug launch
rchiodo Jul 16, 2026
04bd449
Fail fast on None invariants instead of degraded fallbacks
rchiodo Jul 16, 2026
fbb7bd4
Fix messaging response test race
rchiodo Jul 17, 2026
61402bb
Address latest review feedback
rchiodo Jul 17, 2026
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ The tests are run concurrently, and the default number of workers is 8. You can

### Running tests without tox

While tox is the recommended way to run the test suite, pytest can also be invoked directly from the root of the repository. This requires packages in tests/requirements.txt to be installed first.
While tox is the recommended way to run the test suite, pytest can also be invoked directly from the root (src/debugpy) of the repository. This requires packages in tests/requirements.txt to be installed first.

Using a venv created by tox in the '.tox' folder can make it easier to get the pytest configuration correct. Debugpy needs to be installed into the venv for the tests to run, so using the tox generated .venv makes that easier.

Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ ignore = ["src/debugpy/_vendored/pydevd", "src/debugpy/_version.py"]
executionEnvironments = [
{ root = "src" }, { root = "." }
]
typeCheckingMode = "standard"
enableTypeIgnoreComments = false
Comment thread
rchiodo marked this conversation as resolved.

[tool.ruff]
# Enable the pycodestyle (`E`) and Pyflakes (`F`) rules by default.
Expand Down
3 changes: 2 additions & 1 deletion src/debugpy/adapter/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import locale
import os
import sys
from typing import Any

# WARNING: debugpy and submodules must not be imported on top level in this module,
# and should be imported locally inside main() instead.
Expand Down Expand Up @@ -55,7 +56,7 @@ def main():
if args.for_server is None:
adapter.access_token = codecs.encode(os.urandom(32), "hex").decode("ascii")

endpoints = {}
endpoints: dict[str, Any] = {}
try:
client_host, client_port = clients.serve(args.host, args.port)
except Exception as exc:
Expand Down
47 changes: 29 additions & 18 deletions src/debugpy/adapter/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
import atexit
import os
import sys
from typing import Any, Callable, Union, cast

import debugpy
from debugpy import adapter, common, launcher
from debugpy.common import json, log, messaging, sockets
from debugpy.adapter import clients, components, launchers, servers, sessions


class Client(components.Component):
"""Handles the client side of a debug session."""

Expand Down Expand Up @@ -67,7 +67,7 @@ def __init__(self, sock):
fully handled.
"""

self.start_request = None
self.start_request: Union[messaging.Request, None] = None
"""The "launch" or "attach" request as received from the client.
"""

Expand Down Expand Up @@ -124,11 +124,12 @@ def propagate_after_start(self, event):
self.client.channel.propagate(event)

def _propagate_deferred_events(self):
log.debug("Propagating deferred events to {0}...", self.client)
for event in self._deferred_events:
log.debug("Propagating deferred {0}", event.describe())
self.client.channel.propagate(event)
log.info("All deferred events propagated to {0}.", self.client)
if self._deferred_events is not None:
log.debug("Propagating deferred events to {0}...", self.client)
for event in self._deferred_events:
log.debug("Propagating deferred {0}", event.describe())
self.client.channel.propagate(event)
log.info("All deferred events propagated to {0}.", self.client)
self._deferred_events = None

# Generic event handler. There are no specific handlers for client events, because
Expand Down Expand Up @@ -203,9 +204,9 @@ def initialize_request(self, request):
#
# See https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522
# for the sequence of request and events necessary to orchestrate the start.
def _start_message_handler(f):
def _start_message_handler(f: Callable[..., Any])-> Callable[..., object | None]: # pyright: ignore[reportGeneralTypeIssues, reportSelfClsParameterName]
@components.Component.message_handler
def handle(self, request):
def handle(self, request: messaging.Message):
assert request.is_request("launch", "attach")
if self._initialize_request is None:
raise request.isnt_valid("Session is not initialized yet")
Expand All @@ -216,15 +217,16 @@ def handle(self, request):
if self.session.no_debug:
servers.dont_wait_for_first_connection()

request_options: list[Any] = cast("list[Any]", request("debugOptions", json.array(str)))
self.session.debug_options = debug_options = set(
request("debugOptions", json.array(str))
request_options
)

f(self, request)
if request.response is not None:
if isinstance(request, messaging.Request) and request.response is not None:
return

if self.server:
if self.server and isinstance(request, messaging.Request):
self.server.initialize(self._initialize_request)
self._initialize_request = None

Expand Down Expand Up @@ -268,7 +270,7 @@ def handle(self, request):
except messaging.MessageHandlingError as exc:
exc.propagate(request)

if self.session.no_debug:
if self.session.no_debug and isinstance(request, messaging.Request):
self.start_request = request
self.has_started = True
request.respond({})
Expand Down Expand Up @@ -336,6 +338,7 @@ def property_or_debug_option(prop_name, flag_name):
launcher_python = python[0]

program = module = code = ()
args = []
if "program" in request:
program = request("program", str)
args = [program]
Expand Down Expand Up @@ -392,7 +395,7 @@ def property_or_debug_option(prop_name, flag_name):
if cwd == ():
# If it's not specified, but we're launching a file rather than a module,
# and the specified path has a directory in it, use that.
cwd = None if program == () else (os.path.dirname(program) or None)
cwd = None if program == () else (os.path.dirname(str(program)) or None)

sudo = bool(property_or_debug_option("sudo", "Sudo"))
if sudo and sys.platform == "win32":
Expand Down Expand Up @@ -487,6 +490,9 @@ def attach_request(self, request):
else:
if not servers.is_serving():
servers.serve(localhost)
# servers.serve() above guarantees a listener; fail fast if it's missing
# rather than handing the debuggee an empty ("", 0) address it can't use.
assert servers.listener is not None
host, port = sockets.get_address(servers.listener)

# There are four distinct possibilities here.
Expand Down Expand Up @@ -579,9 +585,9 @@ def on_output(category, output):
request.cant_handle("{0} is already being debugged.", conn)

@message_handler
def configurationDone_request(self, request):
def configurationDone_request(self, request: messaging.Request):
if self.start_request is None or self.has_started:
request.cant_handle(
raise request.cant_handle(
'"configurationDone" is only allowed during handling of a "launch" '
'or an "attach" request'
)
Expand Down Expand Up @@ -626,7 +632,8 @@ def evaluate_request(self, request):
def handle_response(response):
request.respond(response.body)

propagated_request.on_response(handle_response)
if propagated_request is not None:
propagated_request.on_response(handle_response)

return messaging.NO_RESPONSE

Expand All @@ -652,7 +659,7 @@ def debugpySystemInfo_request(self, request):
result = {"debugpy": {"version": debugpy.__version__}}
if self.server:
try:
pydevd_info = self.server.channel.request("pydevdSystemInfo")
pydevd_info: messaging.MessageDict = self.server.channel.request("pydevdSystemInfo")
except Exception:
# If the server has already disconnected, or couldn't handle it,
# report what we've got.
Expand Down Expand Up @@ -765,6 +772,10 @@ def notify_of_subprocess(self, conn):
body["connect"]["host"] = host or localhost
if "port" not in body["connect"]:
if port is None:
# A subprocess is only reported once the client is connected, so the
# client listener must be serving; fail fast rather than sending the
# child a null port it can't connect to.
assert listener is not None
_, port = sockets.get_address(listener)
body["connect"]["port"] = port

Expand Down
19 changes: 13 additions & 6 deletions src/debugpy/adapter/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
# for license information.

import functools
from typing import TYPE_CHECKING, Type, TypeVar, Union, cast

if TYPE_CHECKING:
# Dont import this during runtime. There's an order
# of imports issue that causes the debugger to hang.
from debugpy.adapter.sessions import Session
from debugpy.common import json, log, messaging, util


Expand Down Expand Up @@ -31,7 +36,7 @@ class Component(util.Observable):
to wait_for() a change caused by another component.
"""

def __init__(self, session, stream=None, channel=None):
def __init__(self, session: "Session", stream: "Union[messaging.JsonIOStream, None]"=None, channel: "Union[messaging.JsonMessageChannel, None]"=None):
assert (stream is None) ^ (channel is None)

try:
Expand All @@ -44,18 +49,19 @@ def __init__(self, session, stream=None, channel=None):

self.session = session

if channel is None:
if channel is None and stream is not None:
stream.name = str(self)
channel = messaging.JsonMessageChannel(stream, self)
channel.start()
else:
elif channel is not None:
channel.name = channel.stream.name = str(self)
channel.handlers = self
assert channel is not None
self.channel = channel
self.is_connected = True

# Do this last to avoid triggering useless notifications for assignments above.
self.observers += [lambda *_: self.session.notify_changed()]
self.observers = [*self.observers, lambda *_: self.session.notify_changed()]

def __str__(self):
return f"{type(self).__name__}[{self.session.id}]"
Expand Down Expand Up @@ -108,8 +114,9 @@ def disconnect(self):
self.is_connected = False
self.session.finalize("{0} has disconnected".format(self))

T = TypeVar('T')

def missing(session, type):
def missing(session, type: Type[T]) -> T:
class Missing(object):
"""A dummy component that raises ComponentNotAvailable whenever some
attribute is accessed on it.
Expand All @@ -124,7 +131,7 @@ def report():
except Exception as exc:
log.reraise_exception("{0} in {1}", exc, session)

return Missing()
return cast(type, Missing())

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.

📍 src/debugpy/adapter/components.py:134

Standard checking reports reportInvalidTypeForm because the value parameter type cannot be used as a type expression. Cast to the type variable instead (cast(T, Missing())) or otherwise model this factory without using the runtime parameter as a type.

[verified]



class Capabilities(dict):
Expand Down
11 changes: 8 additions & 3 deletions src/debugpy/adapter/launchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# for license information.

import os
import socket
import subprocess
import sys

Expand All @@ -18,7 +19,7 @@ class Launcher(components.Component):

message_handler = components.Component.message_handler

def __init__(self, session, stream):
def __init__(self, session: sessions.Session, stream):
with session:
assert not session.launcher
super().__init__(session, stream)
Expand Down Expand Up @@ -89,11 +90,15 @@ def spawn_debuggee(

arguments = dict(start_request.arguments)
if not session.no_debug:
# For a debug launch the server listener must already be up; fail fast
# rather than silently spawning a debuggee that can't connect back.
assert servers.listener is not None
_, arguments["port"] = sockets.get_address(servers.listener)
arguments["adapterAccessToken"] = adapter.access_token
Comment thread
rchiodo marked this conversation as resolved.

def on_launcher_connected(sock):
listener.close()
def on_launcher_connected(sock: socket.socket):
if listener is not None:
listener.close()
stream = messaging.JsonIOStream.from_socket(sock)
Launcher(session, stream)

Expand Down
Loading
Loading