Vendor debugpy for VS Code attach debugging

Bundles debugpy 1.7.0 directly in python/ so "Maya: Attach (debugpy)"
works with no per-machine pip install step. Installed via Maya 2022's
own pip so it resolved a version actually compatible with Python 3.7
(Maya 2022's interpreter), then verified import + listen() succeeds
under all three target Maya Python versions (3.7/3.9/3.10).

Also fixes a latent __file__-under-exec() bug in
start_debug_server.py (same pitfall as Maya's own plugin loader,
never hit until this exercised it) and corrects the README's Maya
Python version claim -- 2022 ships Python 3.7, not 3.9 as previously
stated, which was never independently verified until now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 09:01:16 +08:00
parent e2f2e6668a
commit ec5f52c7c6
623 changed files with 236815 additions and 12 deletions
@@ -0,0 +1,613 @@
import inspect
from _pydev_bundle import pydev_log
from _pydevd_bundle.pydevd_comm import CMD_SET_BREAK, CMD_ADD_EXCEPTION_BREAK
from _pydevd_bundle.pydevd_constants import STATE_SUSPEND, DJANGO_SUSPEND, \
DebugInfoHolder
from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, FCode, just_raised, ignore_exception_trace
from pydevd_file_utils import canonical_normalized_path, absolute_path
from _pydevd_bundle.pydevd_api import PyDevdAPI
from pydevd_plugins.pydevd_line_validation import LineBreakpointWithLazyValidation, ValidationInfo
from _pydev_bundle.pydev_override import overrides
IS_DJANGO18 = False
IS_DJANGO19 = False
IS_DJANGO19_OR_HIGHER = False
try:
import django
version = django.VERSION
IS_DJANGO18 = version[0] == 1 and version[1] == 8
IS_DJANGO19 = version[0] == 1 and version[1] == 9
IS_DJANGO19_OR_HIGHER = ((version[0] == 1 and version[1] >= 9) or version[0] > 1)
except:
pass
class DjangoLineBreakpoint(LineBreakpointWithLazyValidation):
def __init__(self, canonical_normalized_filename, breakpoint_id, line, condition, func_name, expression, hit_condition=None, is_logpoint=False):
self.canonical_normalized_filename = canonical_normalized_filename
LineBreakpointWithLazyValidation.__init__(self, breakpoint_id, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint)
def __str__(self):
return "DjangoLineBreakpoint: %s-%d" % (self.canonical_normalized_filename, self.line)
class _DjangoValidationInfo(ValidationInfo):
@overrides(ValidationInfo._collect_valid_lines_in_template_uncached)
def _collect_valid_lines_in_template_uncached(self, template):
lines = set()
for node in self._iternodes(template.nodelist):
if node.__class__.__name__ in _IGNORE_RENDER_OF_CLASSES:
continue
lineno = self._get_lineno(node)
if lineno is not None:
lines.add(lineno)
return lines
def _get_lineno(self, node):
if hasattr(node, 'token') and hasattr(node.token, 'lineno'):
return node.token.lineno
return None
def _iternodes(self, nodelist):
for node in nodelist:
yield node
try:
children = node.child_nodelists
except:
pass
else:
for attr in children:
nodelist = getattr(node, attr, None)
if nodelist:
# i.e.: yield from _iternodes(nodelist)
for node in self._iternodes(nodelist):
yield node
def add_line_breakpoint(plugin, pydb, type, canonical_normalized_filename, breakpoint_id, line, condition, expression, func_name, hit_condition=None, is_logpoint=False, add_breakpoint_result=None, on_changed_breakpoint_state=None):
if type == 'django-line':
django_line_breakpoint = DjangoLineBreakpoint(canonical_normalized_filename, breakpoint_id, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint)
if not hasattr(pydb, 'django_breakpoints'):
_init_plugin_breaks(pydb)
if IS_DJANGO19_OR_HIGHER:
add_breakpoint_result.error_code = PyDevdAPI.ADD_BREAKPOINT_LAZY_VALIDATION
django_line_breakpoint.add_breakpoint_result = add_breakpoint_result
django_line_breakpoint.on_changed_breakpoint_state = on_changed_breakpoint_state
else:
add_breakpoint_result.error_code = PyDevdAPI.ADD_BREAKPOINT_NO_ERROR
return django_line_breakpoint, pydb.django_breakpoints
return None
def after_breakpoints_consolidated(plugin, py_db, canonical_normalized_filename, id_to_pybreakpoint, file_to_line_to_breakpoints):
if IS_DJANGO19_OR_HIGHER:
django_breakpoints_for_file = file_to_line_to_breakpoints.get(canonical_normalized_filename)
if not django_breakpoints_for_file:
return
if not hasattr(py_db, 'django_validation_info'):
_init_plugin_breaks(py_db)
# In general we validate the breakpoints only when the template is loaded, but if the template
# was already loaded, we can validate the breakpoints based on the last loaded value.
py_db.django_validation_info.verify_breakpoints_from_template_cached_lines(
py_db, canonical_normalized_filename, django_breakpoints_for_file)
def add_exception_breakpoint(plugin, pydb, type, exception):
if type == 'django':
if not hasattr(pydb, 'django_exception_break'):
_init_plugin_breaks(pydb)
pydb.django_exception_break[exception] = True
return True
return False
def _init_plugin_breaks(pydb):
pydb.django_exception_break = {}
pydb.django_breakpoints = {}
pydb.django_validation_info = _DjangoValidationInfo()
def remove_exception_breakpoint(plugin, pydb, type, exception):
if type == 'django':
try:
del pydb.django_exception_break[exception]
return True
except:
pass
return False
def remove_all_exception_breakpoints(plugin, pydb):
if hasattr(pydb, 'django_exception_break'):
pydb.django_exception_break = {}
return True
return False
def get_breakpoints(plugin, pydb, type):
if type == 'django-line':
return pydb.django_breakpoints
return None
def _inherits(cls, *names):
if cls.__name__ in names:
return True
inherits_node = False
for base in inspect.getmro(cls):
if base.__name__ in names:
inherits_node = True
break
return inherits_node
_IGNORE_RENDER_OF_CLASSES = ('TextNode', 'NodeList')
def _is_django_render_call(frame, debug=False):
try:
name = frame.f_code.co_name
if name != 'render':
return False
if 'self' not in frame.f_locals:
return False
cls = frame.f_locals['self'].__class__
inherits_node = _inherits(cls, 'Node')
if not inherits_node:
return False
clsname = cls.__name__
if IS_DJANGO19:
# in Django 1.9 we need to save the flag that there is included template
if clsname == 'IncludeNode':
if 'context' in frame.f_locals:
context = frame.f_locals['context']
context._has_included_template = True
return clsname not in _IGNORE_RENDER_OF_CLASSES
except:
pydev_log.exception()
return False
def _is_django_context_get_call(frame):
try:
if 'self' not in frame.f_locals:
return False
cls = frame.f_locals['self'].__class__
return _inherits(cls, 'BaseContext')
except:
pydev_log.exception()
return False
def _is_django_resolve_call(frame):
try:
name = frame.f_code.co_name
if name != '_resolve_lookup':
return False
if 'self' not in frame.f_locals:
return False
cls = frame.f_locals['self'].__class__
clsname = cls.__name__
return clsname == 'Variable'
except:
pydev_log.exception()
return False
def _is_django_suspended(thread):
return thread.additional_info.suspend_type == DJANGO_SUSPEND
def suspend_django(main_debugger, thread, frame, cmd=CMD_SET_BREAK):
if frame.f_lineno is None:
return None
main_debugger.set_suspend(thread, cmd)
thread.additional_info.suspend_type = DJANGO_SUSPEND
return frame
def _find_django_render_frame(frame):
while frame is not None and not _is_django_render_call(frame):
frame = frame.f_back
return frame
#=======================================================================================================================
# Django Frame
#=======================================================================================================================
def _read_file(filename):
# type: (str) -> str
f = open(filename, 'r', encoding='utf-8', errors='replace')
s = f.read()
f.close()
return s
def _offset_to_line_number(text, offset):
curLine = 1
curOffset = 0
while curOffset < offset:
if curOffset == len(text):
return -1
c = text[curOffset]
if c == '\n':
curLine += 1
elif c == '\r':
curLine += 1
if curOffset < len(text) and text[curOffset + 1] == '\n':
curOffset += 1
curOffset += 1
return curLine
def _get_source_django_18_or_lower(frame):
# This method is usable only for the Django <= 1.8
try:
node = frame.f_locals['self']
if hasattr(node, 'source'):
return node.source
else:
if IS_DJANGO18:
# The debug setting was changed since Django 1.8
pydev_log.error_once("WARNING: Template path is not available. Set the 'debug' option in the OPTIONS of a DjangoTemplates "
"backend.")
else:
# The debug setting for Django < 1.8
pydev_log.error_once("WARNING: Template path is not available. Please set TEMPLATE_DEBUG=True in your settings.py to make "
"django template breakpoints working")
return None
except:
pydev_log.exception()
return None
def _convert_to_str(s):
return s
def _get_template_original_file_name_from_frame(frame):
try:
if IS_DJANGO19:
# The Node source was removed since Django 1.9
if 'context' in frame.f_locals:
context = frame.f_locals['context']
if hasattr(context, '_has_included_template'):
# if there was included template we need to inspect the previous frames and find its name
back = frame.f_back
while back is not None and frame.f_code.co_name in ('render', '_render'):
locals = back.f_locals
if 'self' in locals:
self = locals['self']
if self.__class__.__name__ == 'Template' and hasattr(self, 'origin') and \
hasattr(self.origin, 'name'):
return _convert_to_str(self.origin.name)
back = back.f_back
else:
if hasattr(context, 'template') and hasattr(context.template, 'origin') and \
hasattr(context.template.origin, 'name'):
return _convert_to_str(context.template.origin.name)
return None
elif IS_DJANGO19_OR_HIGHER:
# For Django 1.10 and later there is much simpler way to get template name
if 'self' in frame.f_locals:
self = frame.f_locals['self']
if hasattr(self, 'origin') and hasattr(self.origin, 'name'):
return _convert_to_str(self.origin.name)
return None
source = _get_source_django_18_or_lower(frame)
if source is None:
pydev_log.debug("Source is None\n")
return None
fname = _convert_to_str(source[0].name)
if fname == '<unknown source>':
pydev_log.debug("Source name is %s\n" % fname)
return None
else:
return fname
except:
if DebugInfoHolder.DEBUG_TRACE_LEVEL >= 2:
pydev_log.exception('Error getting django template filename.')
return None
def _get_template_line(frame):
if IS_DJANGO19_OR_HIGHER:
node = frame.f_locals['self']
if hasattr(node, 'token') and hasattr(node.token, 'lineno'):
return node.token.lineno
else:
return None
source = _get_source_django_18_or_lower(frame)
original_filename = _get_template_original_file_name_from_frame(frame)
if original_filename is not None:
try:
absolute_filename = absolute_path(original_filename)
return _offset_to_line_number(_read_file(absolute_filename), source[1][0])
except:
return None
return None
class DjangoTemplateFrame(object):
IS_PLUGIN_FRAME = True
def __init__(self, frame):
original_filename = _get_template_original_file_name_from_frame(frame)
self._back_context = frame.f_locals['context']
self.f_code = FCode('Django Template', original_filename)
self.f_lineno = _get_template_line(frame)
self.f_back = frame
self.f_globals = {}
self.f_locals = self._collect_context(self._back_context)
self.f_trace = None
def _collect_context(self, context):
res = {}
try:
for d in context.dicts:
for k, v in d.items():
res[k] = v
except AttributeError:
pass
return res
def _change_variable(self, name, value):
for d in self._back_context.dicts:
for k, v in d.items():
if k == name:
d[k] = value
class DjangoTemplateSyntaxErrorFrame(object):
IS_PLUGIN_FRAME = True
def __init__(self, frame, original_filename, lineno, f_locals):
self.f_code = FCode('Django TemplateSyntaxError', original_filename)
self.f_lineno = lineno
self.f_back = frame
self.f_globals = {}
self.f_locals = f_locals
self.f_trace = None
def change_variable(plugin, frame, attr, expression):
if isinstance(frame, DjangoTemplateFrame):
result = eval(expression, frame.f_globals, frame.f_locals)
frame._change_variable(attr, result)
return result
return False
def _is_django_variable_does_not_exist_exception_break_context(frame):
try:
name = frame.f_code.co_name
except:
name = None
return name in ('_resolve_lookup', 'find_template')
def _is_ignoring_failures(frame):
while frame is not None:
if frame.f_code.co_name == 'resolve':
ignore_failures = frame.f_locals.get('ignore_failures')
if ignore_failures:
return True
frame = frame.f_back
return False
#=======================================================================================================================
# Django Step Commands
#=======================================================================================================================
def can_skip(plugin, main_debugger, frame):
if main_debugger.django_breakpoints:
if _is_django_render_call(frame):
return False
if main_debugger.django_exception_break:
module_name = frame.f_globals.get('__name__', '')
if module_name == 'django.template.base':
# Exceptions raised at django.template.base must be checked.
return False
return True
def has_exception_breaks(plugin):
if len(plugin.main_debugger.django_exception_break) > 0:
return True
return False
def has_line_breaks(plugin):
for _canonical_normalized_filename, breakpoints in plugin.main_debugger.django_breakpoints.items():
if len(breakpoints) > 0:
return True
return False
def cmd_step_into(plugin, main_debugger, frame, event, args, stop_info, stop):
info = args[2]
thread = args[3]
plugin_stop = False
if _is_django_suspended(thread):
stop_info['django_stop'] = event == 'call' and _is_django_render_call(frame)
plugin_stop = stop_info['django_stop']
stop = stop and _is_django_resolve_call(frame.f_back) and not _is_django_context_get_call(frame)
if stop:
info.pydev_django_resolve_frame = True # we remember that we've go into python code from django rendering frame
return stop, plugin_stop
def cmd_step_over(plugin, main_debugger, frame, event, args, stop_info, stop):
info = args[2]
thread = args[3]
plugin_stop = False
if _is_django_suspended(thread):
stop_info['django_stop'] = event == 'call' and _is_django_render_call(frame)
plugin_stop = stop_info['django_stop']
stop = False
return stop, plugin_stop
else:
if event == 'return' and info.pydev_django_resolve_frame and _is_django_resolve_call(frame.f_back):
# we return to Django suspend mode and should not stop before django rendering frame
info.pydev_step_stop = frame.f_back
info.pydev_django_resolve_frame = False
thread.additional_info.suspend_type = DJANGO_SUSPEND
stop = info.pydev_step_stop is frame and event in ('line', 'return')
return stop, plugin_stop
def stop(plugin, main_debugger, frame, event, args, stop_info, arg, step_cmd):
main_debugger = args[0]
thread = args[3]
if 'django_stop' in stop_info and stop_info['django_stop']:
frame = suspend_django(main_debugger, thread, DjangoTemplateFrame(frame), step_cmd)
if frame:
main_debugger.do_wait_suspend(thread, frame, event, arg)
return True
return False
def get_breakpoint(plugin, py_db, pydb_frame, frame, event, args):
py_db = args[0]
_filename = args[1]
info = args[2]
breakpoint_type = 'django'
if event == 'call' and info.pydev_state != STATE_SUSPEND and py_db.django_breakpoints and _is_django_render_call(frame):
original_filename = _get_template_original_file_name_from_frame(frame)
pydev_log.debug("Django is rendering a template: %s", original_filename)
canonical_normalized_filename = canonical_normalized_path(original_filename)
django_breakpoints_for_file = py_db.django_breakpoints.get(canonical_normalized_filename)
if django_breakpoints_for_file:
# At this point, let's validate whether template lines are correct.
if IS_DJANGO19_OR_HIGHER:
django_validation_info = py_db.django_validation_info
context = frame.f_locals['context']
django_template = context.template
django_validation_info.verify_breakpoints(py_db, canonical_normalized_filename, django_breakpoints_for_file, django_template)
pydev_log.debug("Breakpoints for that file: %s", django_breakpoints_for_file)
template_line = _get_template_line(frame)
pydev_log.debug("Tracing template line: %s", template_line)
if template_line in django_breakpoints_for_file:
django_breakpoint = django_breakpoints_for_file[template_line]
new_frame = DjangoTemplateFrame(frame)
return True, django_breakpoint, new_frame, breakpoint_type
return False, None, None, breakpoint_type
def suspend(plugin, main_debugger, thread, frame, bp_type):
if bp_type == 'django':
return suspend_django(main_debugger, thread, DjangoTemplateFrame(frame))
return None
def _get_original_filename_from_origin_in_parent_frame_locals(frame, parent_frame_name):
filename = None
parent_frame = frame
while parent_frame.f_code.co_name != parent_frame_name:
parent_frame = parent_frame.f_back
origin = None
if parent_frame is not None:
origin = parent_frame.f_locals.get('origin')
if hasattr(origin, 'name') and origin.name is not None:
filename = _convert_to_str(origin.name)
return filename
def exception_break(plugin, main_debugger, pydb_frame, frame, args, arg):
main_debugger = args[0]
thread = args[3]
exception, value, trace = arg
if main_debugger.django_exception_break and exception is not None:
if exception.__name__ in ['VariableDoesNotExist', 'TemplateDoesNotExist', 'TemplateSyntaxError'] and \
just_raised(trace) and not ignore_exception_trace(trace):
if exception.__name__ == 'TemplateSyntaxError':
# In this case we don't actually have a regular render frame with the context
# (we didn't really get to that point).
token = getattr(value, 'token', None)
if token is None:
# Django 1.7 does not have token in exception. Try to get it from locals.
token = frame.f_locals.get('token')
lineno = getattr(token, 'lineno', None)
original_filename = None
if lineno is not None:
original_filename = _get_original_filename_from_origin_in_parent_frame_locals(frame, 'get_template')
if original_filename is None:
# Django 1.7 does not have origin in get_template. Try to get it from
# load_template.
original_filename = _get_original_filename_from_origin_in_parent_frame_locals(frame, 'load_template')
if original_filename is not None and lineno is not None:
syntax_error_frame = DjangoTemplateSyntaxErrorFrame(
frame, original_filename, lineno, {'token': token, 'exception': exception})
suspend_frame = suspend_django(
main_debugger, thread, syntax_error_frame, CMD_ADD_EXCEPTION_BREAK)
return True, suspend_frame
elif exception.__name__ == 'VariableDoesNotExist':
if _is_django_variable_does_not_exist_exception_break_context(frame):
if not getattr(exception, 'silent_variable_failure', False) and not _is_ignoring_failures(frame):
render_frame = _find_django_render_frame(frame)
if render_frame:
suspend_frame = suspend_django(
main_debugger, thread, DjangoTemplateFrame(render_frame), CMD_ADD_EXCEPTION_BREAK)
if suspend_frame:
add_exception_to_frame(suspend_frame, (exception, value, trace))
thread.additional_info.pydev_message = 'VariableDoesNotExist'
suspend_frame.f_back = frame
frame = suspend_frame
return True, frame
return None
@@ -0,0 +1,30 @@
Extensions allow extending the debugger without modifying the debugger code. This is implemented with explicit namespace
packages.
To implement your own extension:
1. Ensure that the root folder of your extension is in sys.path (add it to PYTHONPATH)
2. Ensure that your module follows the directory structure below
3. The ``__init__.py`` files inside the pydevd_plugin and extension folder must contain the preamble below,
and nothing else.
Preamble:
```python
try:
__import__('pkg_resources').declare_namespace(__name__)
except ImportError:
import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__)
```
4. Your plugin name inside the extensions folder must start with `"pydevd_plugin"`
5. Implement one or more of the abstract base classes defined in `_pydevd_bundle.pydevd_extension_api`. This can be done
by either inheriting from them or registering with the abstract base class.
* Directory structure:
```
|-- root_directory-> must be on python path
| |-- pydevd_plugins
| | |-- __init__.py -> must contain preamble
| | |-- extensions
| | | |-- __init__.py -> must contain preamble
| | | |-- pydevd_plugin_plugin_name.py
```
@@ -0,0 +1,26 @@
import sys
def find_cached_module(mod_name):
return sys.modules.get(mod_name, None)
def find_mod_attr(mod_name, attr):
mod = find_cached_module(mod_name)
if mod is None:
return None
return getattr(mod, attr, None)
def find_class_name(val):
class_name = str(val.__class__)
if class_name.find('.') != -1:
class_name = class_name.split('.')[-1]
elif class_name.find("'") != -1: #does not have '.' (could be something like <type 'int'>)
class_name = class_name[class_name.index("'") + 1:]
if class_name.endswith("'>"):
class_name = class_name[:-2]
return class_name
@@ -0,0 +1,90 @@
from _pydevd_bundle.pydevd_extension_api import TypeResolveProvider
from _pydevd_bundle.pydevd_resolver import defaultResolver
from .pydevd_helpers import find_mod_attr
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_NUMPY_MAX_ITEMS environment variable.'
TOO_LARGE_ATTR = 'Unable to handle:'
class NdArrayItemsContainer(object):
pass
class NDArrayTypeResolveProvider(object):
'''
This resolves a numpy ndarray returning some metadata about the NDArray
'''
def can_provide(self, type_object, type_name):
nd_array = find_mod_attr('numpy', 'ndarray')
return nd_array is not None and issubclass(type_object, nd_array)
def is_numeric(self, obj):
if not hasattr(obj, 'dtype'):
return False
return obj.dtype.kind in 'biufc'
def resolve(self, obj, attribute):
if attribute == '__internals__':
return defaultResolver.get_dictionary(obj)
if attribute == 'min':
if self.is_numeric(obj) and obj.size > 0:
return obj.min()
else:
return None
if attribute == 'max':
if self.is_numeric(obj) and obj.size > 0:
return obj.max()
else:
return None
if attribute == 'shape':
return obj.shape
if attribute == 'dtype':
return obj.dtype
if attribute == 'size':
return obj.size
if attribute.startswith('['):
container = NdArrayItemsContainer()
i = 0
format_str = '%0' + str(int(len(str(len(obj))))) + 'd'
for item in obj:
setattr(container, format_str % i, item)
i += 1
if i >= pydevd_constants.PYDEVD_CONTAINER_NUMPY_MAX_ITEMS:
setattr(container, TOO_LARGE_ATTR, TOO_LARGE_MSG % (pydevd_constants.PYDEVD_CONTAINER_NUMPY_MAX_ITEMS,))
break
return container
return None
def get_dictionary(self, obj):
ret = dict()
ret['__internals__'] = defaultResolver.get_dictionary(obj)
if obj.size > 1024 * 1024:
ret['min'] = 'ndarray too big, calculating min would slow down debugging'
ret['max'] = 'ndarray too big, calculating max would slow down debugging'
elif obj.size == 0:
ret['min'] = 'array is empty'
ret['max'] = 'array is empty'
else:
if self.is_numeric(obj):
ret['min'] = obj.min()
ret['max'] = obj.max()
else:
ret['min'] = 'not a numeric object'
ret['max'] = 'not a numeric object'
ret['shape'] = obj.shape
ret['dtype'] = obj.dtype
ret['size'] = obj.size
try:
ret['[0:%s] ' % (len(obj))] = list(obj[0:pydevd_constants.PYDEVD_CONTAINER_NUMPY_MAX_ITEMS])
except:
# This may not work depending on the array shape.
pass
return ret
import sys
if not sys.platform.startswith("java"):
TypeResolveProvider.register(NDArrayTypeResolveProvider)
@@ -0,0 +1,186 @@
import sys
from _pydevd_bundle.pydevd_constants import PANDAS_MAX_ROWS, PANDAS_MAX_COLS, PANDAS_MAX_COLWIDTH
from _pydevd_bundle.pydevd_extension_api import TypeResolveProvider, StrPresentationProvider
from _pydevd_bundle.pydevd_resolver import inspect, MethodWrapperType
from _pydevd_bundle.pydevd_utils import Timer
from .pydevd_helpers import find_mod_attr
from contextlib import contextmanager
def _get_dictionary(obj, replacements):
ret = dict()
cls = obj.__class__
for attr_name in dir(obj):
# This is interesting but it actually hides too much info from the dataframe.
# attr_type_in_cls = type(getattr(cls, attr_name, None))
# if attr_type_in_cls == property:
# ret[attr_name] = '<property (not computed)>'
# continue
timer = Timer()
try:
replacement = replacements.get(attr_name)
if replacement is not None:
ret[attr_name] = replacement
continue
attr_value = getattr(obj, attr_name, '<unable to get>')
if inspect.isroutine(attr_value) or isinstance(attr_value, MethodWrapperType):
continue
ret[attr_name] = attr_value
except Exception as e:
ret[attr_name] = '<error getting: %s>' % (e,)
finally:
timer.report_if_getting_attr_slow(cls, attr_name)
return ret
@contextmanager
def customize_pandas_options():
# The default repr depends on the settings of:
#
# pandas.set_option('display.max_columns', None)
# pandas.set_option('display.max_rows', None)
#
# which can make the repr **very** slow on some cases, so, we customize pandas to have
# smaller values if the current values are too big.
custom_options = []
from pandas import get_option
max_rows = get_option("display.max_rows")
max_cols = get_option("display.max_columns")
max_colwidth = get_option("display.max_colwidth")
if max_rows is None or max_rows > PANDAS_MAX_ROWS:
custom_options.append("display.max_rows")
custom_options.append(PANDAS_MAX_ROWS)
if max_cols is None or max_cols > PANDAS_MAX_COLS:
custom_options.append("display.max_columns")
custom_options.append(PANDAS_MAX_COLS)
if max_colwidth is None or max_colwidth > PANDAS_MAX_COLWIDTH:
custom_options.append("display.max_colwidth")
custom_options.append(PANDAS_MAX_COLWIDTH)
if custom_options:
from pandas import option_context
with option_context(*custom_options):
yield
else:
yield
class PandasDataFrameTypeResolveProvider(object):
def can_provide(self, type_object, type_name):
data_frame_class = find_mod_attr('pandas.core.frame', 'DataFrame')
return data_frame_class is not None and issubclass(type_object, data_frame_class)
def resolve(self, obj, attribute):
return getattr(obj, attribute)
def get_dictionary(self, obj):
replacements = {
# This actually calls: DataFrame.transpose(), which can be expensive, so,
# let's just add some string representation for it.
'T': '<transposed dataframe -- debugger:skipped eval>',
# This creates a whole new dict{index: Series) for each column. Doing a
# subsequent repr() from this dict can be very slow, so, don't return it.
'_series': '<dict[index:Series] -- debugger:skipped eval>',
'style': '<pandas.io.formats.style.Styler -- debugger: skipped eval>',
}
return _get_dictionary(obj, replacements)
def get_str_in_context(self, df, context:str):
'''
:param context:
This is the context in which the variable is being requested. Valid values:
"watch",
"repl",
"hover",
"clipboard"
'''
if context in ('repl', 'clipboard'):
return repr(df)
return self.get_str(df)
def get_str(self, df):
with customize_pandas_options():
return repr(df)
class PandasSeriesTypeResolveProvider(object):
def can_provide(self, type_object, type_name):
series_class = find_mod_attr('pandas.core.series', 'Series')
return series_class is not None and issubclass(type_object, series_class)
def resolve(self, obj, attribute):
return getattr(obj, attribute)
def get_dictionary(self, obj):
replacements = {
# This actually calls: DataFrame.transpose(), which can be expensive, so,
# let's just add some string representation for it.
'T': '<transposed dataframe -- debugger:skipped eval>',
# This creates a whole new dict{index: Series) for each column. Doing a
# subsequent repr() from this dict can be very slow, so, don't return it.
'_series': '<dict[index:Series] -- debugger:skipped eval>',
'style': '<pandas.io.formats.style.Styler -- debugger: skipped eval>',
}
return _get_dictionary(obj, replacements)
def get_str_in_context(self, df, context:str):
'''
:param context:
This is the context in which the variable is being requested. Valid values:
"watch",
"repl",
"hover",
"clipboard"
'''
if context in ('repl', 'clipboard'):
return repr(df)
return self.get_str(df)
def get_str(self, series):
with customize_pandas_options():
return repr(series)
class PandasStylerTypeResolveProvider(object):
def can_provide(self, type_object, type_name):
series_class = find_mod_attr('pandas.io.formats.style', 'Styler')
return series_class is not None and issubclass(type_object, series_class)
def resolve(self, obj, attribute):
return getattr(obj, attribute)
def get_dictionary(self, obj):
replacements = {
'data': '<Styler data -- debugger:skipped eval>',
'__dict__': '<dict -- debugger: skipped eval>',
}
return _get_dictionary(obj, replacements)
if not sys.platform.startswith("java"):
TypeResolveProvider.register(PandasDataFrameTypeResolveProvider)
StrPresentationProvider.register(PandasDataFrameTypeResolveProvider)
TypeResolveProvider.register(PandasSeriesTypeResolveProvider)
StrPresentationProvider.register(PandasSeriesTypeResolveProvider)
TypeResolveProvider.register(PandasStylerTypeResolveProvider)
@@ -0,0 +1,16 @@
from _pydevd_bundle.pydevd_extension_api import StrPresentationProvider
from .pydevd_helpers import find_mod_attr, find_class_name
class DjangoFormStr(object):
def can_provide(self, type_object, type_name):
form_class = find_mod_attr('django.forms', 'Form')
return form_class is not None and issubclass(type_object, form_class)
def get_str(self, val):
return '%s: %r' % (find_class_name(val), val)
import sys
if not sys.platform.startswith("java"):
StrPresentationProvider.register(DjangoFormStr)
@@ -0,0 +1,506 @@
from _pydevd_bundle.pydevd_constants import STATE_SUSPEND, JINJA2_SUSPEND
from _pydevd_bundle.pydevd_comm import CMD_SET_BREAK, CMD_ADD_EXCEPTION_BREAK
from pydevd_file_utils import canonical_normalized_path
from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, FCode
from _pydev_bundle import pydev_log
from pydevd_plugins.pydevd_line_validation import LineBreakpointWithLazyValidation, ValidationInfo
from _pydev_bundle.pydev_override import overrides
from _pydevd_bundle.pydevd_api import PyDevdAPI
class Jinja2LineBreakpoint(LineBreakpointWithLazyValidation):
def __init__(self, canonical_normalized_filename, breakpoint_id, line, condition, func_name, expression, hit_condition=None, is_logpoint=False):
self.canonical_normalized_filename = canonical_normalized_filename
LineBreakpointWithLazyValidation.__init__(self, breakpoint_id, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint)
def __str__(self):
return "Jinja2LineBreakpoint: %s-%d" % (self.canonical_normalized_filename, self.line)
class _Jinja2ValidationInfo(ValidationInfo):
@overrides(ValidationInfo._collect_valid_lines_in_template_uncached)
def _collect_valid_lines_in_template_uncached(self, template):
lineno_mapping = _get_frame_lineno_mapping(template)
if not lineno_mapping:
return set()
return set(x[0] for x in lineno_mapping)
def add_line_breakpoint(plugin, pydb, type, canonical_normalized_filename, breakpoint_id, line, condition, expression, func_name, hit_condition=None, is_logpoint=False, add_breakpoint_result=None, on_changed_breakpoint_state=None):
if type == 'jinja2-line':
jinja2_line_breakpoint = Jinja2LineBreakpoint(canonical_normalized_filename, breakpoint_id, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint)
if not hasattr(pydb, 'jinja2_breakpoints'):
_init_plugin_breaks(pydb)
add_breakpoint_result.error_code = PyDevdAPI.ADD_BREAKPOINT_LAZY_VALIDATION
jinja2_line_breakpoint.add_breakpoint_result = add_breakpoint_result
jinja2_line_breakpoint.on_changed_breakpoint_state = on_changed_breakpoint_state
return jinja2_line_breakpoint, pydb.jinja2_breakpoints
return None
def after_breakpoints_consolidated(plugin, py_db, canonical_normalized_filename, id_to_pybreakpoint, file_to_line_to_breakpoints):
jinja2_breakpoints_for_file = file_to_line_to_breakpoints.get(canonical_normalized_filename)
if not jinja2_breakpoints_for_file:
return
if not hasattr(py_db, 'jinja2_validation_info'):
_init_plugin_breaks(py_db)
# In general we validate the breakpoints only when the template is loaded, but if the template
# was already loaded, we can validate the breakpoints based on the last loaded value.
py_db.jinja2_validation_info.verify_breakpoints_from_template_cached_lines(
py_db, canonical_normalized_filename, jinja2_breakpoints_for_file)
def add_exception_breakpoint(plugin, pydb, type, exception):
if type == 'jinja2':
if not hasattr(pydb, 'jinja2_exception_break'):
_init_plugin_breaks(pydb)
pydb.jinja2_exception_break[exception] = True
return True
return False
def _init_plugin_breaks(pydb):
pydb.jinja2_exception_break = {}
pydb.jinja2_breakpoints = {}
pydb.jinja2_validation_info = _Jinja2ValidationInfo()
def remove_all_exception_breakpoints(plugin, pydb):
if hasattr(pydb, 'jinja2_exception_break'):
pydb.jinja2_exception_break = {}
return True
return False
def remove_exception_breakpoint(plugin, pydb, type, exception):
if type == 'jinja2':
try:
del pydb.jinja2_exception_break[exception]
return True
except:
pass
return False
def get_breakpoints(plugin, pydb, type):
if type == 'jinja2-line':
return pydb.jinja2_breakpoints
return None
def _is_jinja2_render_call(frame):
try:
name = frame.f_code.co_name
if "__jinja_template__" in frame.f_globals and name in ("root", "loop", "macro") or name.startswith("block_"):
return True
return False
except:
pydev_log.exception()
return False
def _suspend_jinja2(pydb, thread, frame, cmd=CMD_SET_BREAK, message=None):
frame = Jinja2TemplateFrame(frame)
if frame.f_lineno is None:
return None
pydb.set_suspend(thread, cmd)
thread.additional_info.suspend_type = JINJA2_SUSPEND
if cmd == CMD_ADD_EXCEPTION_BREAK:
# send exception name as message
if message:
message = str(message)
thread.additional_info.pydev_message = message
return frame
def _is_jinja2_suspended(thread):
return thread.additional_info.suspend_type == JINJA2_SUSPEND
def _is_jinja2_context_call(frame):
return "_Context__obj" in frame.f_locals
def _is_jinja2_internal_function(frame):
return 'self' in frame.f_locals and frame.f_locals['self'].__class__.__name__ in \
('LoopContext', 'TemplateReference', 'Macro', 'BlockReference')
def _find_jinja2_render_frame(frame):
while frame is not None and not _is_jinja2_render_call(frame):
frame = frame.f_back
return frame
#=======================================================================================================================
# Jinja2 Frame
#=======================================================================================================================
class Jinja2TemplateFrame(object):
IS_PLUGIN_FRAME = True
def __init__(self, frame, original_filename=None, template_lineno=None):
if original_filename is None:
original_filename = _get_jinja2_template_original_filename(frame)
if template_lineno is None:
template_lineno = _get_jinja2_template_line(frame)
self.back_context = None
if 'context' in frame.f_locals:
# sometimes we don't have 'context', e.g. in macros
self.back_context = frame.f_locals['context']
self.f_code = FCode('template', original_filename)
self.f_lineno = template_lineno
self.f_back = frame
self.f_globals = {}
self.f_locals = self.collect_context(frame)
self.f_trace = None
def _get_real_var_name(self, orig_name):
# replace leading number for local variables
parts = orig_name.split('_')
if len(parts) > 1 and parts[0].isdigit():
return parts[1]
return orig_name
def collect_context(self, frame):
res = {}
for k, v in frame.f_locals.items():
if not k.startswith('l_'):
res[k] = v
elif v and not _is_missing(v):
res[self._get_real_var_name(k[2:])] = v
if self.back_context is not None:
for k, v in self.back_context.items():
res[k] = v
return res
def _change_variable(self, frame, name, value):
in_vars_or_parents = False
if 'context' in frame.f_locals:
if name in frame.f_locals['context'].parent:
self.back_context.parent[name] = value
in_vars_or_parents = True
if name in frame.f_locals['context'].vars:
self.back_context.vars[name] = value
in_vars_or_parents = True
l_name = 'l_' + name
if l_name in frame.f_locals:
if in_vars_or_parents:
frame.f_locals[l_name] = self.back_context.resolve(name)
else:
frame.f_locals[l_name] = value
class Jinja2TemplateSyntaxErrorFrame(object):
IS_PLUGIN_FRAME = True
def __init__(self, frame, exception_cls_name, filename, lineno, f_locals):
self.f_code = FCode('Jinja2 %s' % (exception_cls_name,), filename)
self.f_lineno = lineno
self.f_back = frame
self.f_globals = {}
self.f_locals = f_locals
self.f_trace = None
def change_variable(plugin, frame, attr, expression):
if isinstance(frame, Jinja2TemplateFrame):
result = eval(expression, frame.f_globals, frame.f_locals)
frame._change_variable(frame.f_back, attr, result)
return result
return False
def _is_missing(item):
if item.__class__.__name__ == 'MissingType':
return True
return False
def _find_render_function_frame(frame):
# in order to hide internal rendering functions
old_frame = frame
try:
while not ('self' in frame.f_locals and frame.f_locals['self'].__class__.__name__ == 'Template' and \
frame.f_code.co_name == 'render'):
frame = frame.f_back
if frame is None:
return old_frame
return frame
except:
return old_frame
def _get_jinja2_template_debug_info(frame):
frame_globals = frame.f_globals
jinja_template = frame_globals.get('__jinja_template__')
if jinja_template is None:
return None
return _get_frame_lineno_mapping(jinja_template)
def _get_frame_lineno_mapping(jinja_template):
'''
:rtype: list(tuple(int,int))
:return: list((original_line, line_in_frame))
'''
# _debug_info is a string with the mapping from frame line to actual line
# i.e.: "5=13&8=14"
_debug_info = jinja_template._debug_info
if not _debug_info:
# Sometimes template contains only plain text.
return None
# debug_info is a list with the mapping from frame line to actual line
# i.e.: [(5, 13), (8, 14)]
return jinja_template.debug_info
def _get_jinja2_template_line(frame):
debug_info = _get_jinja2_template_debug_info(frame)
if debug_info is None:
return None
lineno = frame.f_lineno
for pair in debug_info:
if pair[1] == lineno:
return pair[0]
return None
def _convert_to_str(s):
return s
def _get_jinja2_template_original_filename(frame):
if '__jinja_template__' in frame.f_globals:
return _convert_to_str(frame.f_globals['__jinja_template__'].filename)
return None
#=======================================================================================================================
# Jinja2 Step Commands
#=======================================================================================================================
def has_exception_breaks(plugin):
if len(plugin.main_debugger.jinja2_exception_break) > 0:
return True
return False
def has_line_breaks(plugin):
for _canonical_normalized_filename, breakpoints in plugin.main_debugger.jinja2_breakpoints.items():
if len(breakpoints) > 0:
return True
return False
def can_skip(plugin, pydb, frame):
if pydb.jinja2_breakpoints and _is_jinja2_render_call(frame):
filename = _get_jinja2_template_original_filename(frame)
if filename is not None:
canonical_normalized_filename = canonical_normalized_path(filename)
jinja2_breakpoints_for_file = pydb.jinja2_breakpoints.get(canonical_normalized_filename)
if jinja2_breakpoints_for_file:
return False
if pydb.jinja2_exception_break:
name = frame.f_code.co_name
# errors in compile time
if name in ('template', 'top-level template code', '<module>') or name.startswith('block '):
f_back = frame.f_back
module_name = ''
if f_back is not None:
module_name = f_back.f_globals.get('__name__', '')
if module_name.startswith('jinja2.'):
return False
return True
def cmd_step_into(plugin, pydb, frame, event, args, stop_info, stop):
info = args[2]
thread = args[3]
plugin_stop = False
stop_info['jinja2_stop'] = False
if _is_jinja2_suspended(thread):
stop_info['jinja2_stop'] = event in ('call', 'line') and _is_jinja2_render_call(frame)
plugin_stop = stop_info['jinja2_stop']
stop = False
if info.pydev_call_from_jinja2 is not None:
if _is_jinja2_internal_function(frame):
# if internal Jinja2 function was called, we sould continue debugging inside template
info.pydev_call_from_jinja2 = None
else:
# we go into python code from Jinja2 rendering frame
stop = True
if event == 'call' and _is_jinja2_context_call(frame.f_back):
# we called function from context, the next step will be in function
info.pydev_call_from_jinja2 = 1
if event == 'return' and _is_jinja2_context_call(frame.f_back):
# we return from python code to Jinja2 rendering frame
info.pydev_step_stop = info.pydev_call_from_jinja2
info.pydev_call_from_jinja2 = None
thread.additional_info.suspend_type = JINJA2_SUSPEND
stop = False
# print "info.pydev_call_from_jinja2", info.pydev_call_from_jinja2, "stop_info", stop_info, \
# "thread.additional_info.suspend_type", thread.additional_info.suspend_type
# print "event", event, "farme.locals", frame.f_locals
return stop, plugin_stop
def cmd_step_over(plugin, pydb, frame, event, args, stop_info, stop):
info = args[2]
thread = args[3]
plugin_stop = False
stop_info['jinja2_stop'] = False
if _is_jinja2_suspended(thread):
stop = False
if info.pydev_call_inside_jinja2 is None:
if _is_jinja2_render_call(frame):
if event == 'call':
info.pydev_call_inside_jinja2 = frame.f_back
if event in ('line', 'return'):
info.pydev_call_inside_jinja2 = frame
else:
if event == 'line':
if _is_jinja2_render_call(frame) and info.pydev_call_inside_jinja2 is frame:
stop_info['jinja2_stop'] = True
plugin_stop = stop_info['jinja2_stop']
if event == 'return':
if frame is info.pydev_call_inside_jinja2 and 'event' not in frame.f_back.f_locals:
info.pydev_call_inside_jinja2 = _find_jinja2_render_frame(frame.f_back)
return stop, plugin_stop
else:
if event == 'return' and _is_jinja2_context_call(frame.f_back):
# we return from python code to Jinja2 rendering frame
info.pydev_call_from_jinja2 = None
info.pydev_call_inside_jinja2 = _find_jinja2_render_frame(frame)
thread.additional_info.suspend_type = JINJA2_SUSPEND
stop = False
return stop, plugin_stop
# print "info.pydev_call_from_jinja2", info.pydev_call_from_jinja2, "stop", stop, "jinja_stop", jinja2_stop, \
# "thread.additional_info.suspend_type", thread.additional_info.suspend_type
# print "event", event, "info.pydev_call_inside_jinja2", info.pydev_call_inside_jinja2
# print "frame", frame, "frame.f_back", frame.f_back, "step_stop", info.pydev_step_stop
# print "is_context_call", _is_jinja2_context_call(frame)
# print "render", _is_jinja2_render_call(frame)
# print "-------------"
return stop, plugin_stop
def stop(plugin, pydb, frame, event, args, stop_info, arg, step_cmd):
pydb = args[0]
thread = args[3]
if 'jinja2_stop' in stop_info and stop_info['jinja2_stop']:
frame = _suspend_jinja2(pydb, thread, frame, step_cmd)
if frame:
pydb.do_wait_suspend(thread, frame, event, arg)
return True
return False
def get_breakpoint(plugin, py_db, pydb_frame, frame, event, args):
py_db = args[0]
_filename = args[1]
info = args[2]
break_type = 'jinja2'
if event == 'line' and info.pydev_state != STATE_SUSPEND and py_db.jinja2_breakpoints and _is_jinja2_render_call(frame):
jinja_template = frame.f_globals.get('__jinja_template__')
if jinja_template is None:
return False, None, None, break_type
original_filename = _get_jinja2_template_original_filename(frame)
if original_filename is not None:
pydev_log.debug("Jinja2 is rendering a template: %s", original_filename)
canonical_normalized_filename = canonical_normalized_path(original_filename)
jinja2_breakpoints_for_file = py_db.jinja2_breakpoints.get(canonical_normalized_filename)
if jinja2_breakpoints_for_file:
jinja2_validation_info = py_db.jinja2_validation_info
jinja2_validation_info.verify_breakpoints(py_db, canonical_normalized_filename, jinja2_breakpoints_for_file, jinja_template)
template_lineno = _get_jinja2_template_line(frame)
if template_lineno is not None:
jinja2_breakpoint = jinja2_breakpoints_for_file.get(template_lineno)
if jinja2_breakpoint is not None:
new_frame = Jinja2TemplateFrame(frame, original_filename, template_lineno)
return True, jinja2_breakpoint, new_frame, break_type
return False, None, None, break_type
def suspend(plugin, pydb, thread, frame, bp_type):
if bp_type == 'jinja2':
return _suspend_jinja2(pydb, thread, frame)
return None
def exception_break(plugin, pydb, pydb_frame, frame, args, arg):
pydb = args[0]
thread = args[3]
exception, value, trace = arg
if pydb.jinja2_exception_break and exception is not None:
exception_type = list(pydb.jinja2_exception_break.keys())[0]
if exception.__name__ in ('UndefinedError', 'TemplateNotFound', 'TemplatesNotFound'):
# errors in rendering
render_frame = _find_jinja2_render_frame(frame)
if render_frame:
suspend_frame = _suspend_jinja2(pydb, thread, render_frame, CMD_ADD_EXCEPTION_BREAK, message=exception_type)
if suspend_frame:
add_exception_to_frame(suspend_frame, (exception, value, trace))
suspend_frame.f_back = frame
frame = suspend_frame
return True, frame
elif exception.__name__ in ('TemplateSyntaxError', 'TemplateAssertionError'):
name = frame.f_code.co_name
# errors in compile time
if name in ('template', 'top-level template code', '<module>') or name.startswith('block '):
f_back = frame.f_back
if f_back is not None:
module_name = f_back.f_globals.get('__name__', '')
if module_name.startswith('jinja2.'):
# Jinja2 translates exception info and creates fake frame on his own
pydb_frame.set_suspend(thread, CMD_ADD_EXCEPTION_BREAK)
add_exception_to_frame(frame, (exception, value, trace))
thread.additional_info.suspend_type = JINJA2_SUSPEND
thread.additional_info.pydev_message = str(exception_type)
return True, frame
return None
@@ -0,0 +1,107 @@
from _pydevd_bundle.pydevd_breakpoints import LineBreakpoint
from _pydevd_bundle.pydevd_api import PyDevdAPI
import bisect
from _pydev_bundle import pydev_log
class LineBreakpointWithLazyValidation(LineBreakpoint):
def __init__(self, *args, **kwargs):
LineBreakpoint.__init__(self, *args, **kwargs)
# This is the _AddBreakpointResult that'll be modified (and then re-sent on the
# on_changed_breakpoint_state).
self.add_breakpoint_result = None
# The signature for the callback should be:
# on_changed_breakpoint_state(breakpoint_id: int, add_breakpoint_result: _AddBreakpointResult)
self.on_changed_breakpoint_state = None
# When its state is checked (in which case it'd call on_changed_breakpoint_state if the
# state changed), we store a cache key in 'verified_cache_key' -- in case it changes
# we'd need to re-verify it (for instance, the template could have changed on disk).
self.verified_cache_key = None
class ValidationInfo(object):
def __init__(self):
self._canonical_normalized_filename_to_last_template_lines = {}
def _collect_valid_lines_in_template(self, template):
# We cache the lines in the template itself. Note that among requests the
# template may be a different instance (because the template contents could be
# changed on disk), but this may still be called multiple times during the
# same render session, so, caching is interesting.
lines_cache = getattr(template, '__pydevd_lines_cache__', None)
if lines_cache is not None:
lines, sorted_lines = lines_cache
return lines, sorted_lines
lines = self._collect_valid_lines_in_template_uncached(template)
lines = frozenset(lines)
sorted_lines = tuple(sorted(lines))
template.__pydevd_lines_cache__ = lines, sorted_lines
return lines, sorted_lines
def _collect_valid_lines_in_template_uncached(self, template):
raise NotImplementedError()
def verify_breakpoints(self, py_db, canonical_normalized_filename, template_breakpoints_for_file, template):
'''
This function should be called whenever a rendering is detected.
:param str canonical_normalized_filename:
:param dict[int:LineBreakpointWithLazyValidation] template_breakpoints_for_file:
'''
valid_lines_frozenset, sorted_lines = self._collect_valid_lines_in_template(template)
self._canonical_normalized_filename_to_last_template_lines[canonical_normalized_filename] = valid_lines_frozenset, sorted_lines
self._verify_breakpoints_with_lines_collected(py_db, canonical_normalized_filename, template_breakpoints_for_file, valid_lines_frozenset, sorted_lines)
def verify_breakpoints_from_template_cached_lines(self, py_db, canonical_normalized_filename, template_breakpoints_for_file):
'''
This is used when the lines are already available (if just the template is available,
`verify_breakpoints` should be used instead).
'''
cached = self._canonical_normalized_filename_to_last_template_lines.get(canonical_normalized_filename)
if cached is not None:
valid_lines_frozenset, sorted_lines = cached
self._verify_breakpoints_with_lines_collected(py_db, canonical_normalized_filename, template_breakpoints_for_file, valid_lines_frozenset, sorted_lines)
def _verify_breakpoints_with_lines_collected(self, py_db, canonical_normalized_filename, template_breakpoints_for_file, valid_lines_frozenset, sorted_lines):
for line, template_bp in list(template_breakpoints_for_file.items()): # Note: iterate in a copy (we may mutate it).
if template_bp.verified_cache_key != valid_lines_frozenset:
template_bp.verified_cache_key = valid_lines_frozenset
valid = line in valid_lines_frozenset
if not valid:
new_line = -1
if sorted_lines:
# Adjust to the first preceding valid line.
idx = bisect.bisect_left(sorted_lines, line)
if idx > 0:
new_line = sorted_lines[idx - 1]
if new_line >= 0 and new_line not in template_breakpoints_for_file:
# We just add it if found and if there's no existing breakpoint at that
# location.
if template_bp.add_breakpoint_result.error_code != PyDevdAPI.ADD_BREAKPOINT_NO_ERROR and template_bp.add_breakpoint_result.translated_line != new_line:
pydev_log.debug('Template breakpoint in %s in line: %s moved to line: %s', canonical_normalized_filename, line, new_line)
template_bp.add_breakpoint_result.error_code = PyDevdAPI.ADD_BREAKPOINT_NO_ERROR
template_bp.add_breakpoint_result.translated_line = new_line
# Add it to a new line.
template_breakpoints_for_file.pop(line, None)
template_breakpoints_for_file[new_line] = template_bp
template_bp.on_changed_breakpoint_state(template_bp.breakpoint_id, template_bp.add_breakpoint_result)
else:
if template_bp.add_breakpoint_result.error_code != PyDevdAPI.ADD_BREAKPOINT_INVALID_LINE:
pydev_log.debug('Template breakpoint in %s in line: %s invalid (valid lines: %s)', canonical_normalized_filename, line, valid_lines_frozenset)
template_bp.add_breakpoint_result.error_code = PyDevdAPI.ADD_BREAKPOINT_INVALID_LINE
template_bp.on_changed_breakpoint_state(template_bp.breakpoint_id, template_bp.add_breakpoint_result)
else:
if template_bp.add_breakpoint_result.error_code != PyDevdAPI.ADD_BREAKPOINT_NO_ERROR:
template_bp.add_breakpoint_result.error_code = PyDevdAPI.ADD_BREAKPOINT_NO_ERROR
template_bp.on_changed_breakpoint_state(template_bp.breakpoint_id, template_bp.add_breakpoint_result)