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,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)