Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more typing to debugpy and switch to 'standard' type checking mode #1637

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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_options = request("debugOptions", json.array(str))
self.session.debug_options = debug_options = set(
request("debugOptions", json.array(str))

rchiodo marked this conversation as resolved.
Show resolved Hide resolved
)

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
rchiodo marked this conversation as resolved.
Show resolved Hide resolved
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())


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

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