This commit is contained in:
Rich Chiodo 2024-07-25 21:46:08 +00:00 committed by GitHub
commit 935376b1f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 275 additions and 169 deletions

View file

@ -65,7 +65,7 @@ On Linux or macOS:
```
.../debugpy$ python3 -m tox
```
This will perform a full run with the default settings. A full run will run tests on Python 2.7 and 3.5-3.8, and requires all of those to be installed. If some versions are missing, or it is desired to skip them for a particular run, tox can be directed to only run tests on specific versions with `-e`. In addition, the `--developer` option can be used to skip the packaging step, running tests directly against the source code in `src/debugpy`. This should only be used when iterating on the code, and a proper run should be performed before submitting a PR. On Windows:
This will perform a full run with the default settings. A full run will run tests on Python 2.7 and 3.5-3.8, and requires all of those to be installed. If some versions are missing, or it is desired to skip them for a particular run, tox can be directed to only run tests on specific versions with `-e`. In addition, the `--develop` option can be used to skip the packaging step, running tests directly against the source code in `src/debugpy`. This should only be used when iterating on the code, and a proper run should be performed before submitting a PR. On Windows:
```
...\debugpy> py -m tox -e py27,py37 --develop
```
@ -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:

View file

@ -21,6 +21,8 @@ ignore = ["src/debugpy/_vendored/pydevd", "src/debugpy/_version.py"]
executionEnvironments = [
{ root = "src" }, { root = "." }
]
typeCheckingMode = "standard"
enableTypeIgnoreComments = false
[tool.ruff]
# Enable the pycodestyle (`E`) and Pyflakes (`F`) rules by default.

View file

@ -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:

View file

@ -7,13 +7,13 @@ from __future__ import annotations
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."""
@ -67,7 +67,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 +124,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 +203,9 @@ 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.
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")
@ -215,15 +216,16 @@ class Client(components.Component):
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
@ -267,7 +269,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 +337,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 +394,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 +487,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 +579,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 +626,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 +653,7 @@ class Client(components.Component):
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.
@ -754,7 +758,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

View file

@ -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
@ -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:
@ -44,13 +49,14 @@ class Component(util.Observable):
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
@ -108,8 +114,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.
@ -124,7 +131,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):

View file

@ -3,6 +3,7 @@
# for license information.
import os
import socket
import subprocess
import sys
@ -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)
@ -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)

View file

@ -5,10 +5,12 @@
from __future__ import annotations
import os
import socket
import subprocess
import sys
import threading
import time
from typing import Callable, Union, cast
import debugpy
from debugpy import adapter
@ -20,7 +22,7 @@ import io
access_token = None
"""Access token used to authenticate with the servers."""
listener = None
listener: Union[socket.socket, None] = None
"""Listener socket that accepts server connections."""
_lock = threading.RLock()
@ -60,7 +62,7 @@ class Connection(object):
channel: messaging.JsonMessageChannel
def __init__(self, sock):
def __init__(self, sock: socket.socket):
from debugpy.adapter import sessions
self.disconnected = False
@ -78,9 +80,10 @@ class Connection(object):
try:
self.authenticate()
info = self.channel.request("pydevdSystemInfo")
process_info = info("process", json.object())
self.pid = process_info("pid", int)
self.ppid = process_info("ppid", int, optional=True)
if not isinstance(info, Exception):
process_info: Callable[..., int] = cast(Callable[..., int], info("process", json.object()))
self.pid = process_info("pid", int)
self.ppid = process_info("ppid", int, optional=True)
if self.ppid == ():
self.ppid = None
self.channel.name = stream.name = str(self)
@ -171,7 +174,7 @@ class Connection(object):
auth = self.channel.request(
"pydevdAuthorize", {"debugServerAccessToken": access_token}
)
if auth["clientAccessToken"] != adapter.access_token:
if not isinstance(auth, Exception) and auth["clientAccessToken"] != adapter.access_token:
self.channel.close()
raise RuntimeError('Mismatched "clientAccessToken"; server not authorized.')
@ -250,7 +253,7 @@ class Server(components.Component):
"supportedChecksumAlgorithms": [],
}
def __init__(self, session, connection):
def __init__(self, session: sessions.Session, connection):
assert connection.server is None
with session:
assert not session.server
@ -283,12 +286,13 @@ class Server(components.Component):
assert request.is_request("initialize")
self.connection.authenticate()
request = self.channel.propagate(request)
request.wait_for_response()
self.capabilities = self.Capabilities(self, request.response)
if request is not None:
request.wait_for_response()
self.capabilities = self.Capabilities(self, request.response)
# 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
@ -418,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)
wait_for_timeout.timed_out = True # pyright: ignore[reportFunctionMemberAccess]
with _lock:
_connections_changed.set()
wait_for_timeout.timed_out = timeout == 0
wait_for_timeout.timed_out = timeout == 0 # pyright: ignore[reportFunctionMemberAccess]
if timeout:
thread = threading.Thread(
target=wait_for_timeout, name="servers.wait_for_connection() timeout"
@ -447,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 wait_for_timeout.timed_out: # pyright: ignore[reportFunctionMemberAccess]
return conn
_connections_changed.wait()
@ -475,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,

View file

@ -7,6 +7,7 @@ import os
import signal
import threading
import time
from typing import Union
from debugpy import common
from debugpy.common import log, util
@ -26,6 +27,7 @@ class Session(util.Observable):
"""
_counter = itertools.count(1)
pid: Union[int, None] = None
def __init__(self):
from debugpy.adapter import clients
@ -94,7 +96,7 @@ class Session(util.Observable):
_sessions.remove(self)
_sessions_changed.set()
def wait_for(self, predicate, timeout=None):
def wait_for(self, predicate, timeout: Union[float, None]=None):
"""Waits until predicate() becomes true.
The predicate is invoked with the session locked. If satisfied, the method
@ -111,13 +113,14 @@ class Session(util.Observable):
seconds regardless of whether the predicate was satisfied. The method returns
False if it timed out, and True otherwise.
"""
def wait_for_timeout():
time.sleep(timeout)
wait_for_timeout.timed_out = True
if timeout is not None:
time.sleep(timeout)
wait_for_timeout.timed_out = True # pyright: ignore[reportFunctionMemberAccess]
self.notify_changed()
wait_for_timeout.timed_out = False
wait_for_timeout.timed_out = False # pyright: ignore[reportFunctionMemberAccess]
if timeout is not None:
thread = threading.Thread(
target=wait_for_timeout, name="Session.wait_for() timeout"
@ -127,7 +130,7 @@ class Session(util.Observable):
with self:
while not predicate():
if wait_for_timeout.timed_out:
if wait_for_timeout.timed_out: # pyright: ignore[reportFunctionMemberAccess]
return False
self._changed_condition.wait()
return True
@ -180,7 +183,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(
@ -218,7 +221,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()
@ -230,7 +234,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

View file

@ -7,8 +7,8 @@
import builtins
import json
import numbers
import operator
from typing import Any, Callable, Literal, Tuple, Union
JsonDecoder = json.JSONDecoder
@ -21,14 +21,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,10 +93,10 @@ class JsonObject(object):
# some substitutions - e.g. replacing () with some default value.
def _converter(value, classinfo):
def _converter(value: str, classinfo) -> Union[int, float, None]:
"""Convert value (str) to number, otherwise return None if is not possible"""
for one_info in classinfo:
if issubclass(one_info, numbers.Number):
if issubclass(one_info, int) or issubclass(one_info, float):
try:
return one_info(value)
except ValueError:
@ -171,7 +171,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 +213,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 +250,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

View file

@ -12,6 +12,12 @@ import platform
import sys
import threading
import traceback
from typing import TYPE_CHECKING, Any, NoReturn, Protocol, Union
if TYPE_CHECKING:
# Careful not force this import in production code, as it's not available in all
# code that we run.
from typing_extensions import TypeIs
import debugpy
from debugpy.common import json, timestamp, util
@ -122,7 +128,7 @@ def newline(level="info"):
stderr.write(level, "\n")
def write(level, text, _to_files=all):
def write(level, text: str, _to_files=all):
assert level in LEVELS
t = timestamp.current()
@ -143,7 +149,7 @@ def write(level, text, _to_files=all):
return text
def write_format(level, format_string, *args, **kwargs):
def write_format(level, format_string: str, *args, **kwargs) -> Union[str, None]:
# Don't spend cycles doing expensive formatting if we don't have to. Errors are
# always formatted, so that error() can return the text even if it's not logged.
if level != "error" and level not in _levels:
@ -215,7 +221,7 @@ def swallow_exception(format_string="", *args, **kwargs):
_exception(format_string, *args, **kwargs)
def reraise_exception(format_string="", *args, **kwargs):
def reraise_exception(format_string="", *args, **kwargs) -> NoReturn:
"""Like swallow_exception(), but re-raises the current exception after logging it."""
assert "exc_info" not in kwargs
@ -278,6 +284,14 @@ def prefixed(format_string, *args, **kwargs):
finally:
_tls.prefix = old_prefix
class HasName(Protocol):
name: str
def has_name(obj: Any) -> "TypeIs[HasName]":
try:
return hasattr(obj, "name")
except NameError:
return False
def get_environment_description(header):
import sysconfig
@ -359,7 +373,11 @@ def get_environment_description(header):
report("Installed packages:\n")
try:
for pkg in importlib_metadata.distributions():
report(" {0}=={1}\n", pkg.name, pkg.version)
if has_name(pkg):
name = pkg.name
report(" {0}=={1}\n", name, pkg.version)
else:
report(" {0}\n", pkg)
except Exception: # pragma: no cover
swallow_exception(
"Error while enumerating installed packages.", level="info"
@ -395,7 +413,8 @@ def _repr(value): # pragma: no cover
def _vars(*names): # pragma: no cover
locals = inspect.currentframe().f_back.f_locals
frame = inspect.currentframe()
locals = frame.f_back.f_locals if frame is not None and frame.f_back is not None else {}
if names:
locals = {name: locals[name] for name in names if name in locals}
warning("$VARS {0!r}", locals)

View file

@ -14,11 +14,17 @@ from __future__ import annotations
import collections
import contextlib
import functools
import io
import itertools
import os
import socket
import sys
import threading
from typing import TYPE_CHECKING, BinaryIO, Callable, Union, cast, Any
if TYPE_CHECKING:
# Careful not force this import in production code, as it's not available in all
# code that we run.
from typing_extensions import TypeIs
from debugpy.common import json, log, util
from debugpy.common.util import hide_thread_from_debugger
@ -86,7 +92,7 @@ class JsonIOStream(object):
return cls(process.stdout, process.stdin, name)
@classmethod
def from_socket(cls, sock, name=None):
def from_socket(cls: type[JsonIOStream], sock: socket.socket, name: Union[str, None]=None):
"""Creates a new instance that sends and receives messages over a socket."""
sock.settimeout(None) # make socket blocking
if name is None:
@ -96,7 +102,7 @@ class JsonIOStream(object):
# sockets is very slow! Although the implementation of readline() itself is
# native code, it calls read(1) in a loop - and that then ultimately calls
# SocketIO.readinto(), which is implemented in Python.
socket_io = sock.makefile("rwb", 0)
socket_io: socket.SocketIO = sock.makefile("rwb", 0)
# SocketIO.close() doesn't close the underlying socket.
def cleanup():
@ -108,7 +114,13 @@ class JsonIOStream(object):
return cls(socket_io, socket_io, name, cleanup)
def __init__(self, reader, writer, name=None, cleanup=lambda: None):
def __init__(
self,
reader: Union[io.RawIOBase, BinaryIO],
writer: Union[io.RawIOBase, BinaryIO],
name: Union[str, None] = None,
cleanup=lambda: None,
):
"""Creates a new JsonIOStream.
reader must be a BytesIO-like object, from which incoming messages will be
@ -158,11 +170,13 @@ class JsonIOStream(object):
except Exception: # pragma: no cover
log.reraise_exception("Error while closing {0} message stream", self.name)
def _log_message(self, dir, data, logger=log.debug):
def _log_message(
self, dir, data, logger: Callable[..., Union[str, None]] = log.debug
):
return logger("{0} {1} {2}", self.name, dir, data)
def _read_line(self, reader):
line = b""
def _read_line(self, reader: Union[io.RawIOBase, BinaryIO]) -> bytes:
line: bytes = b""
while True:
try:
line += reader.readline()
@ -202,6 +216,7 @@ class JsonIOStream(object):
raw_chunks = []
headers = {}
line: Union[bytes, None] = None
while True:
try:
@ -222,9 +237,12 @@ class JsonIOStream(object):
if line == b"":
break
key, _, value = line.partition(b":")
key, _, value = (
line.partition(b":") if line is not None else (b"", b"", b"")
)
headers[key] = value
length = 0
try:
length = int(headers[b"Content-Length"])
if not (0 <= length <= self.MAX_BODY_SIZE):
@ -256,10 +274,11 @@ class JsonIOStream(object):
except Exception: # pragma: no cover
log_message_and_reraise_exception()
try:
body = decoder.decode(body)
except Exception: # pragma: no cover
log_message_and_reraise_exception()
if isinstance(body, str):
try:
body = decoder.decode(body)
except Exception: # pragma: no cover
log_message_and_reraise_exception()
# If parsed successfully, log as JSON for readability.
self._log_message("-->", body)
@ -283,6 +302,7 @@ class JsonIOStream(object):
# information as we already have at the point of the failure. For example,
# if it fails after it is serialized to JSON, log that JSON.
body: Union[str, bytes] = ""
try:
body = encoder.encode(value)
except Exception: # pragma: no cover
@ -295,7 +315,8 @@ class JsonIOStream(object):
try:
while data_written < len(data):
written = writer.write(data[data_written:])
data_written += written
if written is not None:
data_written += written
writer.flush()
except Exception as exc: # pragma: no cover
self._log_message("<--", value, logger=log.swallow_exception)
@ -325,7 +346,7 @@ class MessageDict(collections.OrderedDict):
such guarantee for outgoing messages.
"""
def __init__(self, message, items=None):
def __init__(self, message: Union[Message, None], items: Union[dict, None]=None):
assert message is None or isinstance(message, Message)
if items is None:
@ -383,19 +404,19 @@ class MessageDict(collections.OrderedDict):
try:
value = validate(value)
except (TypeError, ValueError) as exc:
message = Message if self.message is None else self.message
message = Message.empty() if self.message is None else self.message
err = str(exc)
if not err.startswith("["):
err = " " + err
raise message.isnt_valid("{0}{1}", json.repr(key), err)
return value
def _invalid_if_no_key(func):
def _invalid_if_no_key(func: Callable[..., Any]): # pyright: ignore[reportSelfClsParameterName]
def wrap(self, key, *args, **kwargs):
try:
return func(self, key, *args, **kwargs)
except KeyError:
message = Message if self.message is None else self.message
message = Message.empty() if self.message is None else self.message
raise message.isnt_valid("missing property {0!r}", key)
return wrap
@ -407,6 +428,13 @@ class MessageDict(collections.OrderedDict):
del _invalid_if_no_key
class AssociableMessageDict(MessageDict):
def associate_with(self, message: Message):
self.message = message
def is_associable(obj) -> "TypeIs[AssociableMessageDict]":
return isinstance(obj, MessageDict) and hasattr(obj, "associate_with")
def _payload(value):
"""JSON validator for message payload.
@ -421,12 +449,7 @@ def _payload(value):
# Missing payload. Construct a dummy MessageDict, and make it look like it was
# deserialized. See JsonMessageChannel._parse_incoming_message for why it needs
# to have associate_with().
def associate_with(message):
value.message = message
value = MessageDict(None)
value.associate_with = associate_with
value = AssociableMessageDict(None)
return value
@ -451,7 +474,7 @@ class Message(object):
"""
def __str__(self):
return json.repr(self.json) if self.json is not None else repr(self)
return str(json.repr(self.json)) if self.json is not None else repr(self)
def describe(self):
"""A brief description of the message that is enough to identify it.
@ -463,14 +486,17 @@ class Message(object):
raise NotImplementedError
@property
def payload(self) -> MessageDict:
def payload(self) -> MessageDict | Exception:
"""Payload of the message - self.body or self.arguments, depending on the
message type.
"""
raise NotImplementedError
def __call__(self, *args, **kwargs):
def __call__(self, *args, **kwargs) -> MessageDict | Any | int | float:
"""Same as self.payload(...)."""
assert not isinstance(self.payload, Exception)
if args.count == 0 and kwargs == {}:
return self.payload
return self.payload(*args, **kwargs)
def __contains__(self, key):
@ -523,7 +549,10 @@ class Message(object):
def cant_handle(self, *args, **kwargs):
"""Same as self.error(MessageHandlingError, ...)."""
return self.error(MessageHandlingError, *args, **kwargs)
@classmethod
def empty(cls) -> Message:
return Message(None, None)
class Event(Message):
"""Represents an incoming event.
@ -550,12 +579,12 @@ class Event(Message):
the appropriate exception type that applies_to() the Event object.
"""
def __init__(self, channel, seq, event, body, json=None):
def __init__(self, channel, seq, event, body: MessageDict, json=None):
super().__init__(channel, seq, json)
self.event = event
if isinstance(body, MessageDict) and hasattr(body, "associate_with"):
if is_associable(body):
body.associate_with(self)
self.body = body
@ -644,16 +673,16 @@ class Request(Message):
the appropriate exception type that applies_to() the Request object.
"""
def __init__(self, channel, seq, command, arguments, json=None):
def __init__(self, channel, seq, command, arguments: MessageDict, json=None):
super().__init__(channel, seq, json)
self.command = command
if isinstance(arguments, MessageDict) and hasattr(arguments, "associate_with"):
if is_associable(arguments):
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.
@ -753,16 +782,18 @@ class OutgoingRequest(Request):
response to be received, and register a response handler.
"""
_parse = _handle = None
def __init__(self, channel, seq, command, arguments):
super().__init__(channel, seq, command, arguments)
self._response_handlers = []
# Erase the parse and handle methods, as they are not needed for outgoing.
setattr(self, "_parse", None)
setattr(self, "_handle", None)
def describe(self):
return f"{self.seq} request {json.repr(self.command)} to {self.channel}"
def wait_for_response(self, raise_if_failed=True):
def wait_for_response(self, raise_if_failed=True)-> MessageDict:
"""Waits until a response is received for this request, records the Response
object for it in self.response, and returns response.body.
@ -777,8 +808,10 @@ class OutgoingRequest(Request):
while self.response is None:
self.channel._handlers_enqueued.wait()
if raise_if_failed and not self.response.success:
if raise_if_failed and not self.response.success and isinstance( self.response.body, BaseException):
raise self.response.body
assert not isinstance(self.response.body, Exception)
return self.response.body
def on_response(self, response_handler):
@ -864,13 +897,13 @@ class Response(Message):
the appropriate exception type that applies_to() the Response object.
"""
def __init__(self, channel, seq, request, body, json=None):
def __init__(self, channel, seq, request, body: MessageDict | Exception, json=None):
super().__init__(channel, seq, json)
self.request = request
"""The request to which this is the response."""
if isinstance(body, MessageDict) and hasattr(body, "associate_with"):
if is_associable(body):
body.associate_with(self)
self.body = body
"""Body of the response if the request was successful, or an instance
@ -904,8 +937,10 @@ class Response(Message):
"""
if self.success:
return self.body
else:
elif isinstance(self.body, Exception):
raise self.body
else:
raise Exception(self.body)
@staticmethod
def _parse(channel, message_dict, body=None):
@ -1263,7 +1298,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.
@ -1282,7 +1320,7 @@ class JsonMessageChannel(object):
"""
try:
result = self.propagate(message)
if result.is_request():
if result is not None and result.is_request():
result = result.wait_for_response()
return result
except MessageHandlingError as exc:
@ -1336,10 +1374,10 @@ class JsonMessageChannel(object):
# for all JSON objects, and track them so that they can be later wired up to
# the Message they belong to, once it is instantiated.
def object_hook(d):
d = MessageDict(None, d)
d = AssociableMessageDict(None, d)
if "seq" in d:
self._prettify(d)
d.associate_with = associate_with
setattr(d, "associate_with", associate_with)
message_dicts.append(d)
return d
@ -1363,7 +1401,7 @@ class JsonMessageChannel(object):
message_dict = self.stream.read_json(decoder)
assert isinstance(message_dict, MessageDict) # make sure stream used decoder
msg_type = message_dict("type", json.enum("event", "request", "response"))
msg_type: str = cast(str, message_dict("type", json.enum("event", "request", "response")))
parser = self._message_parsers[msg_type]
try:
parser(self, message_dict)
@ -1421,7 +1459,7 @@ class JsonMessageChannel(object):
while True:
with self:
closed = self._closed
if closed:
if closed and self._parser_thread is not None:
# Wait for the parser thread to wrap up and enqueue any remaining
# handlers, if it is still running.
self._parser_thread.join()

View file

@ -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."""
@ -137,9 +141,9 @@ class ThreadSafeSingleton(Singleton):
# with @threadsafe_method. Such methods should perform the necessary locking to
# ensure thread safety for the callers.
@staticmethod
def assert_locked(self):
lock = type(self)._lock
assert lock is not None
assert lock.acquire(blocking=False), (
"ThreadSafeSingleton accessed without locking. Either use with-statement, "
"or if it is a method or property, mark it as @threadsafe_method or with "

View file

@ -5,6 +5,7 @@
import socket
import sys
import threading
from typing import Any, Callable, Union
from debugpy.common import log
from debugpy.common.util import hide_thread_from_debugger
@ -18,7 +19,7 @@ def create_server(host, port=0, backlog=socket.SOMAXCONN, timeout=None):
host = "127.0.0.1"
if port is None:
port = 0
server: Union[socket.socket, None] = None
try:
server = _new_sock()
if port != 0:
@ -37,7 +38,8 @@ def create_server(host, port=0, backlog=socket.SOMAXCONN, timeout=None):
server.settimeout(timeout)
server.listen(backlog)
except Exception: # pragma: no cover
server.close()
if server is not None:
server.close()
raise
return server
@ -87,7 +89,7 @@ def close_socket(sock):
sock.close()
def serve(name, handler, host, port=0, backlog=socket.SOMAXCONN, timeout=None):
def serve(name: str, handler: Callable[[socket.socket], Any], host: str, port: int=0, backlog=socket.SOMAXCONN, timeout: Union[int, None]=None):
"""Accepts TCP connections on the specified host and port, and invokes the
provided handler function for every new connection.
@ -97,7 +99,7 @@ def serve(name, handler, host, port=0, backlog=socket.SOMAXCONN, timeout=None):
assert backlog > 0
try:
listener = create_server(host, port, backlog, timeout)
listener: socket.socket = create_server(host, port, backlog, timeout)
except Exception: # pragma: no cover
log.reraise_exception(
"Error listening for incoming {0} connections on {1}:{2}:", name, host, port

View file

@ -19,7 +19,7 @@ def evaluate(code, path=__file__, mode="eval"):
class Observable(object):
"""An object with change notifications."""
observers = () # used when attributes are set before __init__ is invoked
observers = [] # used when attributes are set before __init__ is invoked
def __init__(self):
self.observers = []
@ -162,3 +162,4 @@ def hide_thread_from_debugger(thread):
if hide_debugpy_internals():
thread.pydev_do_not_trace = True
thread.is_pydev_daemon_thread = True

View file

@ -10,6 +10,7 @@ import struct
import subprocess
import sys
import threading
from typing import Any
from debugpy import launcher
from debugpy.common import log, messaging
@ -34,7 +35,7 @@ returns True, the launcher pauses and waits for user input before exiting.
def describe():
return f"Debuggee[PID={process.pid}]"
return f"Debuggee[PID={process.pid if process is not None else 0}]"
def spawn(process_name, cmdline, env, redirect_output):
@ -47,6 +48,8 @@ def spawn(process_name, cmdline, env, redirect_output):
)
close_fds = set()
stdout_r = 0
stderr_r = 0
try:
if redirect_output:
# subprocess.PIPE behavior can vary substantially depending on Python version
@ -54,7 +57,7 @@ def spawn(process_name, cmdline, env, redirect_output):
stdout_r, stdout_w = os.pipe()
stderr_r, stderr_w = os.pipe()
close_fds |= {stdout_r, stdout_w, stderr_r, stderr_w}
kwargs = dict(stdout=stdout_w, stderr=stderr_w)
kwargs: dict[str, Any] = dict(stdout=stdout_w, stderr=stderr_w)
else:
kwargs = {}
@ -194,7 +197,7 @@ def kill():
def wait_for_exit():
try:
code = process.wait()
code = process.wait() if process is not None else 0
if sys.platform != "win32" and code < 0:
# On POSIX, if the process was terminated by a signal, Popen will use
# a negative returncode to indicate that - but the actual exit code of
@ -241,7 +244,7 @@ def _wait_for_user_input():
log.debug("msvcrt available - waiting for user input via getch()")
sys.stdout.write("Press any key to continue . . . ")
sys.stdout.flush()
msvcrt.getch()
msvcrt.getch() # pyright: ignore[reportPossiblyUnboundVariable, reportAttributeAccessIssue]
else:
log.debug("msvcrt not available - waiting for user input via read()")
sys.stdout.write("Press Enter to continue . . . ")

View file

@ -18,7 +18,7 @@ class CaptureOutput(object):
instances = {}
"""Keys are output categories, values are CaptureOutput instances."""
def __init__(self, whose, category, fd, stream):
def __init__(self, whose, category, fd: int, stream):
assert category not in self.instances
self.instances[category] = self
log.info("Capturing {0} of {1}.", category, whose)
@ -95,7 +95,7 @@ class CaptureOutput(object):
while i < size:
written = self._stream.write(s[i:])
self._stream.flush()
if written == 0:
if written == 0 and self._fd is not None:
# This means that the output stream was closed from the other end.
# Do the same to the debuggee, so that it knows as well.
os.close(self._fd)

View file

@ -64,14 +64,14 @@ def _errcheck(is_error_result=(lambda result: not result)):
def impl(result, func, args):
if is_error_result(result):
log.debug("{0} returned {1}", func.__name__, result)
raise ctypes.WinError()
raise ctypes.WinError() # pyright: ignore[reportAttributeAccessIssue]
else:
return result
return impl
kernel32 = ctypes.windll.kernel32
kernel32 = ctypes.windll.kernel32 # pyright: ignore[reportAttributeAccessIssue]
kernel32.AssignProcessToJobObject.errcheck = _errcheck()
kernel32.AssignProcessToJobObject.restype = BOOL

View file

@ -4,6 +4,7 @@
import codecs
import os
from typing import Any
import pydevd
import socket
import sys
@ -37,37 +38,36 @@ _config_valid_values = {
_adapter_process = None
def _settrace(*args, **kwargs):
log.debug("pydevd.settrace(*{0!r}, **{1!r})", args, kwargs)
# The stdin in notification is not acted upon in debugpy, so, disable it.
kwargs.setdefault("notify_stdin", False)
try:
return pydevd.settrace(*args, **kwargs)
except Exception:
raise
else:
_settrace.called = True
_settrace.called = False
class _settrace():
called = False
def __new__(cls, *args, **kwargs):
log.debug("pydevd.settrace(*{0!r}, **{1!r})", args, kwargs)
# The stdin in notification is not acted upon in debugpy, so, disable it.
kwargs.setdefault("notify_stdin", False)
try:
return pydevd.settrace(*args, **kwargs)
except Exception:
raise
finally:
cls.called = True
def ensure_logging():
"""Starts logging to log.log_dir, if it hasn't already been done."""
if ensure_logging.ensured:
if ensure_logging.ensured: # pyright: ignore[reportFunctionMemberAccess]
return
ensure_logging.ensured = True
ensure_logging.ensured = True # pyright: ignore[reportFunctionMemberAccess]
log.to_file(prefix="debugpy.server")
log.describe_environment("Initial environment:")
if log.log_dir is not None:
pydevd.log_to(log.log_dir + "/debugpy.pydevd.log")
ensure_logging.ensured = False
ensure_logging.ensured = False # pyright: ignore[reportFunctionMemberAccess]
def log_to(path):
if ensure_logging.ensured:
if getattr(ensure_logging, "ensured"):
raise RuntimeError("logging has already begun")
log.debug("log_to{0!r}", (path,))
@ -78,7 +78,7 @@ def log_to(path):
def configure(properties=None, **kwargs):
if _settrace.called:
if getattr(_settrace, "called"):
raise RuntimeError("debug adapter is already running")
ensure_logging()
@ -239,7 +239,10 @@ def listen(address, settrace_kwargs, in_process_debug_adapter=False):
sock.settimeout(None)
sock_io = sock.makefile("rb", 0)
try:
endpoints = json.loads(sock_io.read().decode("utf-8"))
bytes = sock_io.read()
if bytes is None:
raise EOFError("EOF while reading adapter endpoints")
endpoints = json.loads(bytes.decode("utf-8"))
finally:
sock_io.close()
finally:
@ -297,7 +300,7 @@ def connect(address, settrace_kwargs, access_token=None):
_settrace(host=host, port=port, client_access_token=access_token, **settrace_kwargs)
class wait_for_client:
class wait_for_client_cls:
def __call__(self):
ensure_logging()
log.debug("wait_for_client()")
@ -311,12 +314,10 @@ class wait_for_client:
pydevd._wait_for_attach(cancel=cancel_event)
@staticmethod
def cancel():
def cancel() -> None:
raise RuntimeError("wait_for_client() must be called first")
wait_for_client = wait_for_client()
wait_for_client = wait_for_client_cls()
def is_client_connected():
return pydevd._is_attached()
@ -334,6 +335,7 @@ def breakpoint():
stop_at_frame = sys._getframe().f_back
while (
stop_at_frame is not None
and pydb is not None
and pydb.get_file_type(stop_at_frame) == pydb.PYDEV_FILE
):
stop_at_frame = stop_at_frame.f_back
@ -358,7 +360,7 @@ def trace_this_thread(should_trace):
ensure_logging()
log.debug("trace_this_thread({0!r})", should_trace)
pydb = get_global_debugger()
pydb: Any = get_global_debugger()
if should_trace:
pydb.enable_tracing()
else:

View file

@ -11,7 +11,7 @@ __file__ = os.path.abspath(__file__)
_debugpy_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
def attach(setup):
def attach(setup) -> None:
log = None
try:
import sys

View file

@ -7,10 +7,7 @@ import os
import re
import sys
from importlib.util import find_spec
from typing import Any
from typing import Union
from typing import Tuple
from typing import Dict
from typing import Any, Tuple, Union, Dict
# debugpy.__main__ should have preloaded pydevd properly before importing this module.
# Otherwise, some stdlib modules above might have had imported threading before pydevd

View file

@ -4,12 +4,26 @@
import io
import os
import pytest_timeout
import shutil
import sys
from debugpy.common import json, log
def write_title(title, stream=None, sep="~"):
"""Write a section title.
If *stream* is None sys.stderr will be used, *sep* is used to
draw the line.
"""
if stream is None:
stream = sys.stderr
width, height = shutil.get_terminal_size()
fill = int((width - len(title) - 2) / 2)
line = " ".join([sep * fill, title, sep * fill])
if len(line) < width:
line += sep * (width - len(line))
stream.write("\n" + line + "\n")
def dump():
if log.log_dir is None:
return
@ -27,5 +41,5 @@ def dump():
pass
else:
path = os.path.relpath(path, log.log_dir)
pytest_timeout.write_title(path)
write_title(path)
print(s, file=sys.stderr)

View file

@ -7,7 +7,7 @@ import pytest
import pytest_timeout
import sys
from debugpy.common import log
from debugpy.common import log # pyright: ignore[reportAttributeAccessIssue]
import tests
from tests import logs
@ -56,9 +56,8 @@ def pytest_runtest_makereport(item, call):
def pytest_make_parametrize_id(config, val):
return getattr(val, "pytest_id", None)
# If a test times out and pytest tries to print the stacks of where it was hanging,
# we want to print the pydevd log as well. This is not a normal pytest hook - we
# just detour pytest_timeout.dump_stacks directly.
_dump_stacks = pytest_timeout.dump_stacks
pytest_timeout.dump_stacks = lambda: (_dump_stacks(), logs.dump())
pytest_timeout.dump_stacks = lambda terminal: (_dump_stacks(terminal), logs.dump())

View file

@ -10,6 +10,7 @@ pytest-retry
importlib_metadata
psutil
untangle
## Used in Python code that is run/debugged by the tests:
@ -18,3 +19,4 @@ flask
gevent
numpy
requests
typing_extensions