mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Merge 9c7c06b319 into 6e8e5becb2
This commit is contained in:
commit
935376b1f9
23 changed files with 275 additions and 169 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 . . . ")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue