Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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 @@ -76,7 +76,7 @@ On Linux or macOS:

### 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/test_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 modified debugpy in Visual Studio Code
To test integration between debugpy and Visual Studio Code, the latter can be directed to use a custom version of debugpy in lieu of the one bundled with the Python extension. This is done by specifying `"debugAdapterPath"` in `launch.json` - it must point at the root directory of the *package*, which is `src/debugpy` inside the repository:
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ ignore = ["src/debugpy/_vendored/pydevd", "src/debugpy/_version.py"]
executionEnvironments = [
{ root = "src" }, { root = "." }
]
typeCheckingMode = "standard"

[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 @@ -53,7 +54,7 @@ def main(args):
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: 27 additions & 20 deletions src/debugpy/adapter/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

import atexit
import os
import socket
import sys
from typing import Literal, Union

import debugpy
from debugpy import adapter, common, launcher
Expand Down Expand Up @@ -41,7 +43,7 @@ class Expectations(components.Capabilities):
"pathFormat": json.enum("path", optional=True), # we don't support "uri"
}

def __init__(self, sock):
def __init__(self, sock: Union[Literal["stdio"], socket.socket]):
if sock == "stdio":
log.info("Connecting to client over stdio...", self)
self.using_stdio = True
Expand All @@ -67,7 +69,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 +126,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 @@ -202,9 +205,10 @@ 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.
@staticmethod
def _start_message_handler(f):
@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 @@ -215,15 +219,16 @@ def handle(self, request):
if self.session.no_debug:
servers.dont_wait_for_first_connection()

request("debugOptions", json.array(str))
self.session.debug_options = debug_options = set(
request("debugOptions", json.array(str))

Comment thread
rchiodo marked this conversation as resolved.
Outdated
)

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 @@ -267,7 +272,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 @@ -335,6 +340,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 @@ -391,7 +397,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 @@ -484,7 +490,7 @@ def attach_request(self, request):
else:
if not servers.is_serving():
servers.serve()
host, port = servers.listener.getsockname()
host, port = servers.listener.getsockname() if servers.listener is not None else ("", 0)

# There are four distinct possibilities here.
#
Expand Down Expand Up @@ -576,9 +582,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 @@ -623,7 +629,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 @@ -649,7 +656,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.AssociatableMessageDict = 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 @@ -754,7 +761,7 @@ def notify_of_subprocess(self, conn):
if "host" not in body["connect"]:
body["connect"]["host"] = host if host is not None else "127.0.0.1"
if "port" not in body["connect"]:
if port is None:
if port is None and listener is not None:
_, port = listener.getsockname()
body["connect"]["port"] = port

Expand All @@ -770,7 +777,7 @@ def notify_of_subprocess(self, conn):

def serve(host, port):
global listener
listener = sockets.serve("Client", Client, host, port)
listener = sockets.serve("Client", Client, host, port) # type: ignore
Comment thread
rchiodo marked this conversation as resolved.
Outdated
sessions.report_sockets()
return listener.getsockname()

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

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

from debugpy.adapter.sessions import Session
from debugpy.common import json, log, messaging, util


Expand Down Expand Up @@ -31,7 +33,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,13 +46,14 @@ 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

Expand Down Expand Up @@ -108,8 +111,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 +128,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
10 changes: 6 additions & 4 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 @@ -88,12 +89,13 @@ def spawn_debuggee(
env = {}

arguments = dict(start_request.arguments)
if not session.no_debug:
if not session.no_debug and servers.listener is not None:
_, arguments["port"] = servers.listener.getsockname()
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