diff --git a/_pydevd_bundle/pydevd_constants.py b/_pydevd_bundle/pydevd_constants.py index 5a97a25c..b3199f3f 100644 --- a/_pydevd_bundle/pydevd_constants.py +++ b/_pydevd_bundle/pydevd_constants.py @@ -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 diff --git a/_pydevd_bundle/pydevd_resolver.py b/_pydevd_bundle/pydevd_resolver.py index 20a7351a..40959383 100644 --- a/_pydevd_bundle/pydevd_resolver.py +++ b/_pydevd_bundle/pydevd_resolver.py @@ -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." @@ -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] diff --git a/tests_python/test_resolvers.py b/tests_python/test_resolvers.py index 1266caf4..343643df 100644 --- a/tests_python/test_resolvers.py +++ b/tests_python/test_resolvers.py @@ -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]) @@ -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