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,573 @@
|
||||
import os # @NoMove
|
||||
import sys # @NoMove
|
||||
import pytest
|
||||
|
||||
from _pydevd_bundle import pydevd_reload
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
SAMPLE_CODE = """
|
||||
class C:
|
||||
def foo(self):
|
||||
return 0
|
||||
|
||||
@classmethod
|
||||
def bar(cls):
|
||||
return (0, 0)
|
||||
|
||||
@staticmethod
|
||||
def stomp():
|
||||
return (0, 0, 0)
|
||||
|
||||
def unchanged(self):
|
||||
return 'unchanged'
|
||||
"""
|
||||
|
||||
from _pydevd_bundle.pydevd_constants import IS_JYTHON, IS_IRONPYTHON
|
||||
|
||||
|
||||
@pytest.mark.skipif(IS_JYTHON or IS_IRONPYTHON, reason='CPython related test')
|
||||
class Test(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
unittest.TestCase.setUp(self)
|
||||
self.tempdir = None
|
||||
self.save_path = None
|
||||
self.tempdir = tempfile.mkdtemp()
|
||||
self.save_path = list(sys.path)
|
||||
sys.path.append(self.tempdir)
|
||||
try:
|
||||
del sys.modules['x']
|
||||
except:
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
unittest.TestCase.tearDown(self)
|
||||
sys.path = self.save_path
|
||||
try:
|
||||
del sys.modules['x']
|
||||
except:
|
||||
pass
|
||||
|
||||
def make_mod(self, name="x", repl=None, subst=None, sample=SAMPLE_CODE):
|
||||
basedir = self.tempdir
|
||||
if '.' in name:
|
||||
splitted = name.split('.')
|
||||
basedir = os.path.join(self.tempdir, *splitted[:-1])
|
||||
name = splitted[-1]
|
||||
try:
|
||||
os.makedirs(basedir)
|
||||
except:
|
||||
pass
|
||||
|
||||
fn = os.path.join(basedir, name + ".py")
|
||||
f = open(fn, "w")
|
||||
if repl is not None and subst is not None:
|
||||
sample = sample.replace(repl, subst)
|
||||
try:
|
||||
f.write(sample)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def test_pydevd_reload(self):
|
||||
|
||||
self.make_mod()
|
||||
import x # @UnresolvedImport
|
||||
|
||||
C = x.C
|
||||
COut = C
|
||||
Cfoo = C.foo
|
||||
Cbar = C.bar
|
||||
Cstomp = C.stomp
|
||||
|
||||
def check2(expected):
|
||||
C = x.C
|
||||
Cfoo = C.foo
|
||||
Cbar = C.bar
|
||||
Cstomp = C.stomp
|
||||
b = C()
|
||||
bfoo = b.foo
|
||||
self.assertEqual(expected, b.foo())
|
||||
self.assertEqual(expected, bfoo())
|
||||
self.assertEqual(expected, Cfoo(b))
|
||||
|
||||
def check(expected):
|
||||
b = COut()
|
||||
bfoo = b.foo
|
||||
self.assertEqual(expected, b.foo())
|
||||
self.assertEqual(expected, bfoo())
|
||||
self.assertEqual(expected, Cfoo(b))
|
||||
self.assertEqual((expected, expected), Cbar())
|
||||
self.assertEqual((expected, expected, expected), Cstomp())
|
||||
check2(expected)
|
||||
|
||||
check(0)
|
||||
|
||||
# modify mod and reload
|
||||
count = 0
|
||||
while count < 1:
|
||||
count += 1
|
||||
self.make_mod(repl="0", subst=str(count))
|
||||
pydevd_reload.xreload(x)
|
||||
check(count)
|
||||
|
||||
def test_pydevd_reload2(self):
|
||||
|
||||
self.make_mod()
|
||||
import x # @UnresolvedImport
|
||||
|
||||
c = x.C()
|
||||
cfoo = c.foo
|
||||
self.assertEqual(0, c.foo())
|
||||
self.assertEqual(0, cfoo())
|
||||
|
||||
self.make_mod(repl="0", subst='1')
|
||||
pydevd_reload.xreload(x)
|
||||
self.assertEqual(1, c.foo())
|
||||
self.assertEqual(1, cfoo())
|
||||
|
||||
def test_pydevd_reload3(self):
|
||||
|
||||
class F:
|
||||
|
||||
def m1(self):
|
||||
return 1
|
||||
|
||||
class G:
|
||||
|
||||
def m1(self):
|
||||
return 2
|
||||
|
||||
self.assertEqual(F().m1(), 1)
|
||||
pydevd_reload.Reload(None)._update(None, None, F, G)
|
||||
self.assertEqual(F().m1(), 2)
|
||||
|
||||
def test_pydevd_reload4(self):
|
||||
|
||||
class F:
|
||||
pass
|
||||
|
||||
F.m1 = lambda a:None
|
||||
|
||||
class G:
|
||||
pass
|
||||
|
||||
G.m1 = lambda a:10
|
||||
|
||||
self.assertEqual(F().m1(), None)
|
||||
pydevd_reload.Reload(None)._update(None, None, F, G)
|
||||
self.assertEqual(F().m1(), 10)
|
||||
|
||||
def test_if_code_obj_equals(self):
|
||||
|
||||
class F:
|
||||
|
||||
def m1(self):
|
||||
return 1
|
||||
|
||||
class G:
|
||||
|
||||
def m1(self):
|
||||
return 1
|
||||
|
||||
class H:
|
||||
|
||||
def m1(self):
|
||||
return 2
|
||||
|
||||
if hasattr(F.m1, 'func_code'):
|
||||
self.assertTrue(pydevd_reload.code_objects_equal(F.m1.func_code, G.m1.func_code))
|
||||
self.assertFalse(pydevd_reload.code_objects_equal(F.m1.func_code, H.m1.func_code))
|
||||
else:
|
||||
self.assertTrue(pydevd_reload.code_objects_equal(F.m1.__code__, G.m1.__code__))
|
||||
self.assertFalse(pydevd_reload.code_objects_equal(F.m1.__code__, H.m1.__code__))
|
||||
|
||||
def test_metaclass(self):
|
||||
|
||||
class Meta(type):
|
||||
|
||||
def __init__(cls, name, bases, attrs):
|
||||
super(Meta, cls).__init__(name, bases, attrs)
|
||||
|
||||
class F:
|
||||
__metaclass__ = Meta
|
||||
|
||||
def m1(self):
|
||||
return 1
|
||||
|
||||
class G:
|
||||
__metaclass__ = Meta
|
||||
|
||||
def m1(self):
|
||||
return 2
|
||||
|
||||
self.assertEqual(F().m1(), 1)
|
||||
pydevd_reload.Reload(None)._update(None, None, F, G)
|
||||
self.assertEqual(F().m1(), 2)
|
||||
|
||||
def test_change_hierarchy(self):
|
||||
|
||||
class F(object):
|
||||
|
||||
def m1(self):
|
||||
return 1
|
||||
|
||||
class B(object):
|
||||
|
||||
def super_call(self):
|
||||
return 2
|
||||
|
||||
class G(B):
|
||||
|
||||
def m1(self):
|
||||
return self.super_call()
|
||||
|
||||
self.assertEqual(F().m1(), 1)
|
||||
old = pydevd_reload.notify_error
|
||||
self._called = False
|
||||
|
||||
def on_error(*args):
|
||||
self._called = True
|
||||
|
||||
try:
|
||||
pydevd_reload.notify_error = on_error
|
||||
pydevd_reload.Reload(None)._update(None, None, F, G)
|
||||
self.assertTrue(self._called)
|
||||
finally:
|
||||
pydevd_reload.notify_error = old
|
||||
|
||||
def test_change_hierarchy_old_style(self):
|
||||
|
||||
class F:
|
||||
|
||||
def m1(self):
|
||||
return 1
|
||||
|
||||
class B:
|
||||
|
||||
def super_call(self):
|
||||
return 2
|
||||
|
||||
class G(B):
|
||||
|
||||
def m1(self):
|
||||
return self.super_call()
|
||||
|
||||
self.assertEqual(F().m1(), 1)
|
||||
old = pydevd_reload.notify_error
|
||||
self._called = False
|
||||
|
||||
def on_error(*args):
|
||||
self._called = True
|
||||
|
||||
try:
|
||||
pydevd_reload.notify_error = on_error
|
||||
pydevd_reload.Reload(None)._update(None, None, F, G)
|
||||
self.assertTrue(self._called)
|
||||
finally:
|
||||
pydevd_reload.notify_error = old
|
||||
|
||||
def test_create_class(self):
|
||||
SAMPLE_CODE1 = """
|
||||
class C:
|
||||
def foo(self):
|
||||
return 0
|
||||
"""
|
||||
# Creating a new class and using it from old class
|
||||
SAMPLE_CODE2 = """
|
||||
class B:
|
||||
pass
|
||||
|
||||
class C:
|
||||
def foo(self):
|
||||
return B
|
||||
"""
|
||||
|
||||
self.make_mod(sample=SAMPLE_CODE1)
|
||||
import x # @UnresolvedImport
|
||||
foo = x.C().foo
|
||||
self.assertEqual(foo(), 0)
|
||||
self.make_mod(sample=SAMPLE_CODE2)
|
||||
pydevd_reload.xreload(x)
|
||||
self.assertEqual(foo().__name__, 'B')
|
||||
|
||||
def test_create_class2(self):
|
||||
SAMPLE_CODE1 = """
|
||||
class C(object):
|
||||
def foo(self):
|
||||
return 0
|
||||
"""
|
||||
# Creating a new class and using it from old class
|
||||
SAMPLE_CODE2 = """
|
||||
class B(object):
|
||||
pass
|
||||
|
||||
class C(object):
|
||||
def foo(self):
|
||||
return B
|
||||
"""
|
||||
|
||||
self.make_mod(sample=SAMPLE_CODE1)
|
||||
import x # @UnresolvedImport
|
||||
foo = x.C().foo
|
||||
self.assertEqual(foo(), 0)
|
||||
self.make_mod(sample=SAMPLE_CODE2)
|
||||
pydevd_reload.xreload(x)
|
||||
self.assertEqual(foo().__name__, 'B')
|
||||
|
||||
def test_parent_function(self):
|
||||
SAMPLE_CODE1 = """
|
||||
class B(object):
|
||||
def foo(self):
|
||||
return 0
|
||||
|
||||
class C(B):
|
||||
def call(self):
|
||||
return self.foo()
|
||||
"""
|
||||
# Creating a new class and using it from old class
|
||||
SAMPLE_CODE2 = """
|
||||
class B(object):
|
||||
def foo(self):
|
||||
return 0
|
||||
def bar(self):
|
||||
return 'bar'
|
||||
|
||||
class C(B):
|
||||
def call(self):
|
||||
return self.bar()
|
||||
"""
|
||||
|
||||
self.make_mod(sample=SAMPLE_CODE1)
|
||||
import x # @UnresolvedImport
|
||||
call = x.C().call
|
||||
self.assertEqual(call(), 0)
|
||||
self.make_mod(sample=SAMPLE_CODE2)
|
||||
pydevd_reload.xreload(x)
|
||||
self.assertEqual(call(), 'bar')
|
||||
|
||||
def test_update_constant(self):
|
||||
SAMPLE_CODE1 = """
|
||||
CONSTANT = 1
|
||||
|
||||
class B(object):
|
||||
def foo(self):
|
||||
return CONSTANT
|
||||
"""
|
||||
SAMPLE_CODE2 = """
|
||||
CONSTANT = 2
|
||||
|
||||
class B(object):
|
||||
def foo(self):
|
||||
return CONSTANT
|
||||
"""
|
||||
|
||||
self.make_mod(sample=SAMPLE_CODE1)
|
||||
import x # @UnresolvedImport
|
||||
foo = x.B().foo
|
||||
self.assertEqual(foo(), 1)
|
||||
self.make_mod(sample=SAMPLE_CODE2)
|
||||
pydevd_reload.xreload(x)
|
||||
self.assertEqual(foo(), 1) # Just making it explicit we don't reload constants.
|
||||
|
||||
def test_update_constant_with_custom_code(self):
|
||||
SAMPLE_CODE1 = """
|
||||
CONSTANT = 1
|
||||
|
||||
class B(object):
|
||||
def foo(self):
|
||||
return CONSTANT
|
||||
"""
|
||||
SAMPLE_CODE2 = """
|
||||
CONSTANT = 2
|
||||
|
||||
def __xreload_old_new__(namespace, name, old, new):
|
||||
if name == 'CONSTANT':
|
||||
namespace[name] = new
|
||||
|
||||
class B(object):
|
||||
def foo(self):
|
||||
return CONSTANT
|
||||
"""
|
||||
|
||||
self.make_mod(sample=SAMPLE_CODE1)
|
||||
import x # @UnresolvedImport
|
||||
foo = x.B().foo
|
||||
self.assertEqual(foo(), 1)
|
||||
self.make_mod(sample=SAMPLE_CODE2)
|
||||
pydevd_reload.xreload(x)
|
||||
self.assertEqual(foo(), 2) # Actually updated it now!
|
||||
|
||||
def test_reload_custom_code_after_changes(self):
|
||||
SAMPLE_CODE1 = """
|
||||
CONSTANT = 1
|
||||
|
||||
class B(object):
|
||||
def foo(self):
|
||||
return CONSTANT
|
||||
"""
|
||||
SAMPLE_CODE2 = """
|
||||
CONSTANT = 1
|
||||
|
||||
def __xreload_after_reload_update__(namespace):
|
||||
namespace['CONSTANT'] = 2
|
||||
|
||||
class B(object):
|
||||
def foo(self):
|
||||
return CONSTANT
|
||||
"""
|
||||
|
||||
self.make_mod(sample=SAMPLE_CODE1)
|
||||
import x # @UnresolvedImport
|
||||
foo = x.B().foo
|
||||
self.assertEqual(foo(), 1)
|
||||
self.make_mod(sample=SAMPLE_CODE2)
|
||||
pydevd_reload.xreload(x)
|
||||
self.assertEqual(foo(), 2) # Actually updated it now!
|
||||
|
||||
def test_reload_custom_code_after_changes_in_class(self):
|
||||
SAMPLE_CODE1 = """
|
||||
|
||||
class B(object):
|
||||
CONSTANT = 1
|
||||
|
||||
def foo(self):
|
||||
return self.CONSTANT
|
||||
"""
|
||||
SAMPLE_CODE2 = """
|
||||
|
||||
|
||||
class B(object):
|
||||
CONSTANT = 1
|
||||
|
||||
@classmethod
|
||||
def __xreload_after_reload_update__(cls):
|
||||
cls.CONSTANT = 2
|
||||
|
||||
def foo(self):
|
||||
return self.CONSTANT
|
||||
"""
|
||||
|
||||
self.make_mod(sample=SAMPLE_CODE1)
|
||||
import x # @UnresolvedImport
|
||||
foo = x.B().foo
|
||||
self.assertEqual(foo(), 1)
|
||||
self.make_mod(sample=SAMPLE_CODE2)
|
||||
pydevd_reload.xreload(x)
|
||||
self.assertEqual(foo(), 2) # Actually updated it now!
|
||||
|
||||
def test_update_constant_with_custom_code2(self):
|
||||
SAMPLE_CODE1 = """
|
||||
|
||||
class B(object):
|
||||
CONSTANT = 1
|
||||
|
||||
def foo(self):
|
||||
return self.CONSTANT
|
||||
"""
|
||||
SAMPLE_CODE2 = """
|
||||
|
||||
|
||||
class B(object):
|
||||
|
||||
CONSTANT = 2
|
||||
|
||||
def __xreload_old_new__(cls, name, old, new):
|
||||
if name == 'CONSTANT':
|
||||
cls.CONSTANT = new
|
||||
__xreload_old_new__ = classmethod(__xreload_old_new__)
|
||||
|
||||
def foo(self):
|
||||
return self.CONSTANT
|
||||
"""
|
||||
|
||||
self.make_mod(sample=SAMPLE_CODE1)
|
||||
import x # @UnresolvedImport
|
||||
foo = x.B().foo
|
||||
self.assertEqual(foo(), 1)
|
||||
self.make_mod(sample=SAMPLE_CODE2)
|
||||
pydevd_reload.xreload(x)
|
||||
self.assertEqual(foo(), 2) # Actually updated it now!
|
||||
|
||||
def test_update_with_slots(self):
|
||||
SAMPLE_CODE1 = """
|
||||
class B(object):
|
||||
|
||||
__slots__ = ['bar']
|
||||
|
||||
"""
|
||||
SAMPLE_CODE2 = """
|
||||
class B(object):
|
||||
|
||||
__slots__ = ['bar', 'foo']
|
||||
|
||||
def m1(self):
|
||||
self.bar = 10
|
||||
return 1
|
||||
|
||||
"""
|
||||
|
||||
self.make_mod(sample=SAMPLE_CODE1)
|
||||
import x # @UnresolvedImport
|
||||
B = x.B
|
||||
self.make_mod(sample=SAMPLE_CODE2)
|
||||
pydevd_reload.xreload(x)
|
||||
b = B()
|
||||
self.assertEqual(1, b.m1())
|
||||
self.assertEqual(10, b.bar)
|
||||
self.assertRaises(Exception, setattr, b, 'foo', 20) # __slots__ can't be updated
|
||||
|
||||
def test_reload_numpy(self):
|
||||
SAMPLE_CODE1 = """
|
||||
import numpy as np
|
||||
global_numpy = np.array([1, 2, 3])
|
||||
def method():
|
||||
return 1
|
||||
"""
|
||||
SAMPLE_CODE2 = """
|
||||
import numpy as np
|
||||
global_numpy = np.array([1, 2, 3, 4])
|
||||
def method():
|
||||
return 2
|
||||
"""
|
||||
|
||||
self.make_mod(sample=SAMPLE_CODE1)
|
||||
import x # @UnresolvedImport
|
||||
assert str(x.global_numpy) == '[1 2 3]'
|
||||
self.make_mod(sample=SAMPLE_CODE2)
|
||||
pydevd_reload.xreload(x)
|
||||
# Note that we don't patch globals (the user could do that in a module,
|
||||
# but he'd have to create a custom `__xreload_old_new__` method to
|
||||
# do it).
|
||||
assert str(x.global_numpy) == '[1 2 3]'
|
||||
|
||||
def test_reload_relative(self):
|
||||
MODULE_CODE = """
|
||||
def add_text(s):
|
||||
return s + " module"
|
||||
"""
|
||||
MODULE1_CODE = """
|
||||
from . import module
|
||||
|
||||
def add_more_text(s):
|
||||
s = module.add_text(s)
|
||||
return s + ' module1'
|
||||
"""
|
||||
|
||||
MODULE1_CODE_V2 = """
|
||||
from . import module
|
||||
|
||||
def add_more_text(s):
|
||||
s = module.add_text(s)
|
||||
return s + ' module1V2'
|
||||
"""
|
||||
|
||||
self.make_mod(sample='', name='package.__init__')
|
||||
self.make_mod(sample=MODULE_CODE, name='package.module')
|
||||
self.make_mod(sample=MODULE1_CODE, name='package.module1')
|
||||
from package import module1 # @UnresolvedImport
|
||||
assert module1.add_more_text('1') == '1 module module1'
|
||||
|
||||
self.make_mod(sample=MODULE1_CODE_V2, name='package.module1')
|
||||
pydevd_reload.xreload(module1)
|
||||
assert module1.add_more_text('1') == '1 module module1V2'
|
||||
Reference in New Issue
Block a user