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:
@@ -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
|
||||
|
||||
+90
@@ -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)
|
||||
+186
@@ -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)
|
||||
+16
@@ -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)
|
||||
Reference in New Issue
Block a user