diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f3f4b09a..10098503 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 ``` diff --git a/pyproject.toml b/pyproject.toml index 3addae67..b2011ad7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ executionEnvironments = [ { root = "src" }, { root = "." } ] typeCheckingMode = "standard" +enableTypeIgnoreComments = false [tool.ruff] # Enable the pycodestyle (`E`) and Pyflakes (`F`) rules by default. diff --git a/src/debugpy/adapter/servers.py b/src/debugpy/adapter/servers.py index 219a85a6..b581d4e3 100644 --- a/src/debugpy/adapter/servers.py +++ b/src/debugpy/adapter/servers.py @@ -19,8 +19,6 @@ from debugpy.adapter import components, sessions import traceback import io -from debugpy.common.util import WaitForTimeout - access_token = None """Access token used to authenticate with the servers.""" @@ -431,11 +429,14 @@ def wait_for_connection(session, predicate, timeout: Union[float, None]=None): If there is more than one server connection already available, returns the oldest one. """ - def after_wait(): + def wait_for_timeout(): + 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 = WaitForTimeout(timeout, after_wait) + 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" @@ -450,7 +451,7 @@ def wait_for_connection(session, predicate, timeout: Union[float, None]=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() diff --git a/src/debugpy/adapter/sessions.py b/src/debugpy/adapter/sessions.py index 7b17b93a..5b3ea748 100644 --- a/src/debugpy/adapter/sessions.py +++ b/src/debugpy/adapter/sessions.py @@ -6,6 +6,7 @@ import itertools import os import signal import threading +import time from typing import Union from debugpy import common @@ -112,8 +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. """ - wait_for_timeout = util.WaitForTimeout(timeout, lambda: self.notify_changed()) + def wait_for_timeout(): + 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 # pyright: ignore[reportFunctionMemberAccess] + if timeout is not None: thread = threading.Thread( target=wait_for_timeout, name="Session.wait_for() timeout" @@ -123,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 diff --git a/src/debugpy/common/log.py b/src/debugpy/common/log.py index 343cf50f..d52c9295 100644 --- a/src/debugpy/common/log.py +++ b/src/debugpy/common/log.py @@ -12,8 +12,12 @@ import platform import sys import threading import traceback -from typing import Any, NoReturn, Protocol, Union -from typing_extensions import TypeIs +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 @@ -283,7 +287,7 @@ def prefixed(format_string, *args, **kwargs): class HasName(Protocol): name: str -def has_name(obj: Any) -> TypeIs[HasName]: +def has_name(obj: Any) -> "TypeIs[HasName]": try: return hasattr(obj, "name") except NameError: diff --git a/src/debugpy/common/messaging.py b/src/debugpy/common/messaging.py index 39e46ddf..a0428232 100644 --- a/src/debugpy/common/messaging.py +++ b/src/debugpy/common/messaging.py @@ -20,8 +20,11 @@ import os import socket import sys import threading -from typing import BinaryIO, Callable, Union, cast, Any -from typing_extensions import TypeIs +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 @@ -429,7 +432,7 @@ class AssociableMessageDict(MessageDict): def associate_with(self, message: Message): self.message = message -def is_associable(obj) -> TypeIs[AssociableMessageDict]: +def is_associable(obj) -> "TypeIs[AssociableMessageDict]": return isinstance(obj, MessageDict) and hasattr(obj, "associate_with") def _payload(value): diff --git a/src/debugpy/common/util.py b/src/debugpy/common/util.py index b87d7640..bc519320 100644 --- a/src/debugpy/common/util.py +++ b/src/debugpy/common/util.py @@ -165,14 +165,3 @@ def hide_thread_from_debugger(thread): thread.pydev_do_not_trace = True thread.is_pydev_daemon_thread = True -class WaitForTimeout(): - def __init__(self, timeout: Union[float, None], func: Callable[[], None]): - self._func = func - self._timeout = timeout - self.timed_out = False - - def __call__(self): - if self._timeout is not None: - time.sleep(self._timeout) - self.timed_out = True - self._func() diff --git a/tests/logs.py b/tests/logs.py index 7d3089ca..3b7f6d74 100644 --- a/tests/logs.py +++ b/tests/logs.py @@ -4,12 +4,27 @@ import io import os +import shutil import pytest_timeout 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 +42,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) diff --git a/tests/pytest_hooks.py b/tests/pytest_hooks.py index f37eecb8..7ef3851f 100644 --- a/tests/pytest_hooks.py +++ b/tests/pytest_hooks.py @@ -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()) diff --git a/tests/requirements.txt b/tests/requirements.txt index 5b8a498c..7d48234e 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -19,3 +19,4 @@ flask gevent numpy requests +typing_extensions \ No newline at end of file