diff --git a/src/debugpy/adapter/__main__.py b/src/debugpy/adapter/__main__.py index e18ecd56..06d601fc 100644 --- a/src/debugpy/adapter/__main__.py +++ b/src/debugpy/adapter/__main__.py @@ -8,6 +8,7 @@ import codecs 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. @@ -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: diff --git a/src/debugpy/adapter/clients.py b/src/debugpy/adapter/clients.py index ee1d1514..afd9257b 100644 --- a/src/debugpy/adapter/clients.py +++ b/src/debugpy/adapter/clients.py @@ -6,7 +6,9 @@ from __future__ import annotations import atexit import os +import socket import sys +from typing import Literal, Union import debugpy from debugpy import adapter, common, launcher @@ -41,7 +43,7 @@ class Client(components.Component): "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 @@ -67,7 +69,7 @@ class Client(components.Component): fully handled. """ - self.start_request = None + self.start_request: Union[messaging.Request, None] = None """The "launch" or "attach" request as received from the client. """ @@ -124,11 +126,12 @@ class Client(components.Component): 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 @@ -202,9 +205,10 @@ class Client(components.Component): # # 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") @@ -215,15 +219,16 @@ class Client(components.Component): 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)) + ) 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 @@ -267,7 +272,7 @@ class Client(components.Component): 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({}) @@ -335,6 +340,7 @@ class Client(components.Component): launcher_python = python[0] program = module = code = () + args = [] if "program" in request: program = request("program", str) args = [program] @@ -391,7 +397,7 @@ class Client(components.Component): 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": @@ -484,7 +490,7 @@ class Client(components.Component): 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. # @@ -576,9 +582,9 @@ class Client(components.Component): 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' ) @@ -623,7 +629,8 @@ class Client(components.Component): 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 @@ -649,7 +656,7 @@ class Client(components.Component): 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. @@ -754,7 +761,7 @@ class Client(components.Component): 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 @@ -770,7 +777,7 @@ class Client(components.Component): def serve(host, port): global listener - listener = sockets.serve("Client", Client, host, port) + listener = sockets.serve("Client", Client, host, port) # type: ignore sessions.report_sockets() return listener.getsockname() diff --git a/src/debugpy/adapter/components.py b/src/debugpy/adapter/components.py index 038e8883..448713fa 100644 --- a/src/debugpy/adapter/components.py +++ b/src/debugpy/adapter/components.py @@ -3,7 +3,7 @@ # for license information. import functools -from typing import Union +from typing import Type, TypeVar, Union, cast from debugpy.adapter.sessions import Session from debugpy.common import json, log, messaging, util @@ -33,7 +33,7 @@ class Component(util.Observable): to wait_for() a change caused by another component. """ - def __init__(self, session: Session, stream: Union[messaging.JsonIOStream, None]=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: @@ -53,6 +53,7 @@ class Component(util.Observable): 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 @@ -110,8 +111,9 @@ class Component(util.Observable): 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. @@ -126,7 +128,7 @@ def missing(session, type): except Exception as exc: log.reraise_exception("{0} in {1}", exc, session) - return Missing() + return cast(type, Missing()) class Capabilities(dict): diff --git a/src/debugpy/adapter/servers.py b/src/debugpy/adapter/servers.py index 03ef6d46..996784b9 100644 --- a/src/debugpy/adapter/servers.py +++ b/src/debugpy/adapter/servers.py @@ -292,7 +292,7 @@ class Server(components.Component): # Generic request handler, used if there's no specific handler below. @message_handler - def request(self, request): + def request(self, request: messaging.Message): # Do not delegate requests from the server by default. There is a security # boundary between the server and the adapter, and we cannot trust arbitrary # requests sent over that boundary, since they may contain arbitrary code @@ -397,7 +397,7 @@ class Server(components.Component): def serve(host="127.0.0.1", port=0): global listener - listener = sockets.serve("Server", Connection, host, port) + listener = sockets.serve("Server", Connection, host, port) # type: ignore sessions.report_sockets() return listener.getsockname() @@ -422,21 +422,21 @@ def connections(): return list(_connections) -def wait_for_connection(session, predicate, timeout=None): +def wait_for_connection(session, predicate, timeout: Union[float, None]=None): """Waits until there is a server matching the specified predicate connected to this adapter, and returns the corresponding Connection. If there is more than one server connection already available, returns the oldest one. """ - def wait_for_timeout(): - time.sleep(timeout) - wait_for_timeout.timed_out = True + if timeout is not None: + time.sleep(timeout) + setattr(wait_for_timeout, "timed_out", True) with _lock: _connections_changed.set() - wait_for_timeout.timed_out = timeout == 0 + setattr(wait_for_timeout, "timed_out", timeout == 0) if timeout: thread = threading.Thread( target=wait_for_timeout, name="servers.wait_for_connection() timeout" @@ -451,7 +451,7 @@ def wait_for_connection(session, predicate, timeout=None): _connections_changed.clear() conns = (conn for conn in _connections if predicate(conn)) conn = next(conns, None) - if conn is not None or wait_for_timeout.timed_out: + if conn is not None or getattr(wait_for_timeout, "timed_out") is True: return conn _connections_changed.wait() @@ -479,7 +479,7 @@ def dont_wait_for_first_connection(): def inject(pid, debugpy_args, on_output): - host, port = listener.getsockname() + host, port = listener.getsockname() if listener is not None else ("", 0) cmdline = [ sys.executable, diff --git a/src/debugpy/adapter/sessions.py b/src/debugpy/adapter/sessions.py index 24b492b9..0eaeb750 100644 --- a/src/debugpy/adapter/sessions.py +++ b/src/debugpy/adapter/sessions.py @@ -7,7 +7,7 @@ import os import signal import threading import time -from typing import Union +from typing import Union, cast from debugpy import common from debugpy.common import log, util @@ -182,7 +182,7 @@ class Session(util.Observable): # can ask the launcher to kill it, do so instead of disconnecting # from the server to prevent debuggee from running any more code. self.launcher.terminate_debuggee() - else: + elif self.server.channel is not None: # Otherwise, let the server handle it the best it can. try: self.server.channel.request( @@ -220,7 +220,8 @@ class Session(util.Observable): self.wait_for(lambda: not self.launcher.is_connected) try: - self.launcher.channel.close() + if self.launcher.channel is not None: + self.launcher.channel.close() except Exception: log.swallow_exception() @@ -232,7 +233,8 @@ class Session(util.Observable): if self.client.restart_requested: body["restart"] = True try: - self.client.channel.send_event("terminated", body) + if self.client.channel is not None: + self.client.channel.send_event("terminated", body) except Exception: pass diff --git a/src/debugpy/common/json.py b/src/debugpy/common/json.py index 6f3e2b21..2de77aca 100644 --- a/src/debugpy/common/json.py +++ b/src/debugpy/common/json.py @@ -9,6 +9,7 @@ import builtins import json import numbers import operator +from typing import Any, Callable, Literal, Tuple, Union JsonDecoder = json.JSONDecoder @@ -21,14 +22,14 @@ class JsonEncoder(json.JSONEncoder): result is serialized instead of the object itself. """ - def default(self, value): + def default(self, o): try: - get_state = value.__getstate__ + get_state = o.__getstate__ except AttributeError: pass else: return get_state() - return super().default(value) + return super().default(o) class JsonObject(object): @@ -93,12 +94,12 @@ class JsonObject(object): # some substitutions - e.g. replacing () with some default value. -def _converter(value, classinfo): +def _converter(value: str, classinfo) -> Any: """Convert value (str) to number, otherwise return None if is not possible""" for one_info in classinfo: if issubclass(one_info, numbers.Number): try: - return one_info(value) + return one_info(value) # pyright: ignore except ValueError: pass @@ -171,7 +172,7 @@ def enum(*values, **kwargs): return validate -def array(validate_item=False, vectorize=False, size=None): +def array(validate_item: Union[Callable[..., Any], Literal[False]]=False, vectorize=False, size=None): """Returns a validator for a JSON array. If the property is missing, it is treated as if it were []. Otherwise, it must @@ -213,11 +214,11 @@ def array(validate_item=False, vectorize=False, size=None): ) elif isinstance(size, tuple): assert 1 <= len(size) <= 2 - size = tuple(operator.index(n) for n in size) - min_len, max_len = (size + (None,))[0:2] + sizes = tuple(operator.index(n) for n in size) + min_len, max_len = (sizes + (None,))[0:2] validate_size = lambda value: ( "must have at least {0} elements".format(min_len) - if len(value) < min_len + if min_len is None or len(value) < min_len else "must have at most {0} elements".format(max_len) if max_len is not None and len(value) < max_len else True @@ -250,7 +251,7 @@ def array(validate_item=False, vectorize=False, size=None): return validate -def object(validate_value=False): +def object(validate_value: Union[Callable[..., Any], Tuple, Literal[False]]=False): """Returns a validator for a JSON object. If the property is missing, it is treated as if it were {}. Otherwise, it must diff --git a/src/debugpy/common/messaging.py b/src/debugpy/common/messaging.py index 644c1ba2..3429040a 100644 --- a/src/debugpy/common/messaging.py +++ b/src/debugpy/common/messaging.py @@ -344,7 +344,7 @@ class MessageDict(collections.OrderedDict): such guarantee for outgoing messages. """ - def __init__(self, message, items=None): + def __init__(self, message, items: Union[dict, None]=None): assert message is None or isinstance(message, Message) if items is None: @@ -683,7 +683,7 @@ class Request(Message): arguments.associate_with(self) self.arguments = arguments - self.response = None + self.response: Union[Response, None] = None """Response to this request. For incoming requests, it is set as soon as the request handler returns. @@ -809,7 +809,7 @@ class OutgoingRequest(Request): while self.response is None: self.channel._handlers_enqueued.wait() - if raise_if_failed and not self.response.success and isinstance( self.response.body, Exception): + if raise_if_failed and not self.response.success and isinstance( self.response.body, BaseException): raise self.response.body return self.response.body @@ -1297,7 +1297,10 @@ class JsonMessageChannel(object): def request(self, *args, **kwargs): """Same as send_request(...).wait_for_response()""" - return self.send_request(*args, **kwargs).wait_for_response() + # This should always raise an exception on failure + result = self.send_request(*args, **kwargs).wait_for_response() + assert not isinstance(result, BaseException) + return result def propagate(self, message): """Sends a new message with the same type and payload. diff --git a/src/debugpy/common/singleton.py b/src/debugpy/common/singleton.py index d515a4ab..f8da9ec3 100644 --- a/src/debugpy/common/singleton.py +++ b/src/debugpy/common/singleton.py @@ -86,12 +86,16 @@ class Singleton(object): def __enter__(self): """Lock this singleton to prevent concurrent access.""" - type(self)._lock.acquire() + lock = type(self)._lock + assert lock is not None + lock.acquire() return self def __exit__(self, exc_type, exc_value, exc_tb): """Unlock this singleton to allow concurrent access.""" - type(self)._lock.release() + lock = type(self)._lock + assert lock is not None + lock.release() def share(self): """Share this singleton, if it was originally created with shared=False.""" @@ -138,7 +142,7 @@ class ThreadSafeSingleton(Singleton): # ensure thread safety for the callers. @staticmethod - def assert_locked(self): + def assert_locked(self): # type: ignore lock = type(self)._lock assert lock.acquire(blocking=False), ( "ThreadSafeSingleton accessed without locking. Either use with-statement, "