Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add a timeout when resolving an object's properties. #299

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions _pydevd_bundle/pydevd_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,10 @@ def as_int_in_env(env_key, default):
# on how the thread interruption works (there are some caveats related to it).
PYDEVD_INTERRUPT_THREAD_TIMEOUT = as_float_in_env("PYDEVD_INTERRUPT_THREAD_TIMEOUT", -1)

# Timeout used to prevent class properties from resolving too slowly
# A value <= 0 means this is disabled.
PYDEVD_PROPERTY_RESOLVE_TIMEOUT = as_float_in_env("PYDEVD_PROPERTY_RESOLVE_TIMEOUT", 1.0)

# If PYDEVD_APPLY_PATCHING_TO_HIDE_PYDEVD_THREADS is set to False, the patching to hide pydevd threads won't be applied.
PYDEVD_APPLY_PATCHING_TO_HIDE_PYDEVD_THREADS = (
os.getenv("PYDEVD_APPLY_PATCHING_TO_HIDE_PYDEVD_THREADS", "true").lower() in ENV_TRUE_LOWER_VALUES
Expand Down
26 changes: 24 additions & 2 deletions _pydevd_bundle/pydevd_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
DebugInfoHolder,
IS_PYPY,
GENERATED_LEN_ATTR_NAME,
get_global_debugger,
)
from _pydevd_bundle.pydevd_safe_repr import SafeRepr
from _pydevd_bundle.pydevd_timeout import create_interrupt_this_thread_callback
from _pydevd_bundle import pydevd_constants

TOO_LARGE_MSG = "Maximum number of items (%s) reached. To show more items customize the value of the PYDEVD_CONTAINER_RANDOM_ACCESS_MAX_ITEMS environment variable."
Expand Down Expand Up @@ -184,12 +186,32 @@ def _get_py_dictionary(self, var, names=None, used___dict__=False):
timer = Timer()
cls = type(var)
for name in names:
name_as_str = name
try:
name_as_str = name
if name_as_str.__class__ != str:
name_as_str = "%r" % (name_as_str,)

if not used___dict__:
class_attr_type = None
if hasattr(var.__class__, name_as_str):
class_attr = getattr(var.__class__, name)
class_attr_type = type(class_attr)

timeout_sec = pydevd_constants.PYDEVD_PROPERTY_RESOLVE_TIMEOUT
if (
class_attr_type is property
and timeout_sec > 0
and (py_db := get_global_debugger())
):
try:
on_interrupt_timeout = create_interrupt_this_thread_callback()
timeout_tracker = py_db.timeout_tracker
with timeout_tracker.call_on_timeout(
timeout_sec, on_interrupt_timeout
):
attr = getattr(var, name)
except KeyboardInterrupt:
attr = f"Timeout resolving {class_attr_type.__name__} attribute: {name}"
elif not used___dict__:
attr = getattr(var, name)
else:
attr = var.__dict__[name]
Expand Down
39 changes: 38 additions & 1 deletion tests_python/test_resolvers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from _pydevd_bundle.pydevd_constants import IS_PY36_OR_GREATER, GENERATED_LEN_ATTR_NAME
from _pydevd_bundle import pydevd_constants, pydevd_frame_utils
from pydevd import PyDB
from time import sleep
import pytest
import sys


def check_len_entry(len_entry, first_2_params):
assert len_entry[:2] == first_2_params
assert callable(len_entry[2])
Expand Down Expand Up @@ -124,6 +125,42 @@ def __init__(self):
assert contents_debug_adapter_protocol == [("(1, 2)", (3, 4), ".__dict__[(1, 2)]")]


def test_object_resolver_slow_property():
from _pydevd_bundle.pydevd_resolver import DefaultResolver

default_resolver = DefaultResolver()

class MyObject(object):
def __init__(self):
self.a = 10
self.b = 20

@property
def c(self):
sleep(0.2)
return 30

@property
def d(self):
return 40

pydevd_constants.PYDEVD_PROPERTY_RESOLVE_TIMEOUT = 0.1
_py_db = PyDB()
obj = MyObject()
dictionary = clear_contents_dictionary(default_resolver.get_dictionary(obj))
assert dictionary == {"a": 10, "b": 20, "c": "Timeout resolving property attribute: c", "d": 40}

contents_debug_adapter_protocol = clear_contents_debug_adapter_protocol(
default_resolver.get_contents_debug_adapter_protocol(obj)
)
assert contents_debug_adapter_protocol == [
("a", 10, ".a"),
("b", 20, ".b"),
("c", "Timeout resolving property attribute: c", ".c"),
("d", 40, ".d"),
]


def test_django_forms_resolver():
from _pydevd_bundle.pydevd_resolver import DjangoFormResolver

Expand Down