From 081fe8cc4e8ee41a147f1dc8364039adc76dfb59 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Mon, 11 Apr 2022 16:09:04 -0400 Subject: [PATCH] Allow using pydevd as a regular dependency. This should make it easier to test using PyDev-Debugger (pydevd) as a standalone dependency rather than as a bundled (current status) one, which comes with binaries checked in Git. This in turn should make solving issue 179 limited in scope to PyDev-Debugger itself. * setup.py (DEBUGPY_BUNDLING_DISABLED): New variable. [DEBUGPY_BUNDLING_DISABLED]: Conditionalize 'debugpy._vendored' import. (PYDEVD_ROOT): Set only when DEBUGPY_BUNDLING_DISABLED is False. (ExtModules.__bool__): Make the return value the negation of DEBUGPY_BUNDLING_DISABLED. (__main__)[DEBUGPY_BUNDLING_DISABLED]: New condition to call 'cython_build'. Etch a __bundling_disabled__ variable on the debugpy module. : Do not include the _vendored submodule in the packages and package_data when DEBUGPY_BUNDLING_DISABLED is set. Adjust has_ext_modules argument. * src/debugpy/__init__.py: New '__bundling_disabled__' variable. * src/debugpy/server/__init__.py [debugpy.__bundling_disabled__]: Conditionalize 'debugpy._vendored.force_pydevd' import. * src/debugpy/server/attach_pid_injected.py (attach): Import the attach_script procedure from sys.path directly when debugpy.__bundling_disabled__ is true and do not do any sys.path manipulation in this case. * tests/tests/test_vendoring.py: Skip test if 'debugpy.__bundling_disabled__' is true. * src/debugpy/server/__init__.py: Partially copy the pydevd loading logic from src/debugpy/_vendored/force_pydevd.py. --- setup.py | 57 ++++++++++++++++------- src/debugpy/__init__.py | 2 + src/debugpy/server/__init__.py | 46 +++++++++++++++++- src/debugpy/server/attach_pid_injected.py | 33 +++++++------ tests/tests/test_vendoring.py | 5 ++ 5 files changed, 111 insertions(+), 32 deletions(-) diff --git a/setup.py b/setup.py index 5fc40070f..3a530a29b 100644 --- a/setup.py +++ b/setup.py @@ -11,6 +11,9 @@ import sys +DEBUGPY_BUNDLING_DISABLED = bool(os.getenv('DEBUGPY_BUNDLING_DISABLED')) + + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import versioneer # noqa @@ -18,12 +21,15 @@ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src")) import debugpy -import debugpy._vendored + +if not DEBUGPY_BUNDLING_DISABLED: + import debugpy._vendored del sys.path[0] -PYDEVD_ROOT = debugpy._vendored.project_root("pydevd") +PYDEVD_ROOT = (None if DEBUGPY_BUNDLING_DISABLED else + debugpy._vendored.project_root("pydevd")) DEBUGBY_ROOT = os.path.dirname(os.path.abspath(debugpy.__file__)) @@ -67,7 +73,7 @@ def iter_vendored_files(): # relevant setuptools versions. class ExtModules(list): def __bool__(self): - return True + return not DEBUGPY_BUNDLING_DISABLED def override_build(cmds): @@ -133,9 +139,24 @@ def tail_is(*suffixes): if __name__ == "__main__": - if not os.getenv("SKIP_CYTHON_BUILD"): + if not (os.getenv("SKIP_CYTHON_BUILD") or DEBUGPY_BUNDLING_DISABLED): cython_build() + # Etch bundling status in the source. + if debugpy.__bundling_disabled__ != DEBUGPY_BUNDLING_DISABLED: + + with open(os.path.join(DEBUGBY_ROOT, '__init__.py'), 'r') as f: + lines = f.readlines() + with open(os.path.join(DEBUGBY_ROOT, '__init__.py'), 'w') as f: + edited = [] + for line in lines: + if line.startswith('__bundling_disabled__'): + edited.append( + f'__bundling_disabled__ = {DEBUGPY_BUNDLING_DISABLED}\n') + else: + edited.append(line) + f.writelines(edited) + extras = {} platforms = get_buildplatform() if platforms is not None: @@ -145,6 +166,18 @@ def tail_is(*suffixes): override_build(cmds) override_build_py(cmds) + data = {"debugpy": ["ThirdPartyNotices.txt"]} + packages = [ + "debugpy", + "debugpy.adapter", + "debugpy.common", + "debugpy.launcher", + "debugpy.server", + ] + if not DEBUGPY_BUNDLING_DISABLED: + data.update({"debugpy._vendored": list(iter_vendored_files())}) + packages.append("debugpy._vendored") + setuptools.setup( name="debugpy", version=versioneer.get_version(), @@ -173,20 +206,10 @@ def tail_is(*suffixes): "License :: OSI Approved :: MIT License", ], package_dir={"": "src"}, - packages=[ - "debugpy", - "debugpy.adapter", - "debugpy.common", - "debugpy.launcher", - "debugpy.server", - "debugpy._vendored", - ], - package_data={ - "debugpy": ["ThirdPartyNotices.txt"], - "debugpy._vendored": list(iter_vendored_files()), - }, + packages=packages, + package_data=data, ext_modules=ExtModules(), - has_ext_modules=lambda: True, + has_ext_modules=lambda: not DEBUGPY_BUNDLING_DISABLED, cmdclass=cmds, **extras ) diff --git a/src/debugpy/__init__.py b/src/debugpy/__init__.py index baa5a7c52..7b7a29aa8 100644 --- a/src/debugpy/__init__.py +++ b/src/debugpy/__init__.py @@ -206,6 +206,8 @@ def trace_this_thread(should_trace): __version__ = _version.get_versions()["version"] +__bundling_disabled__ = False + # Force absolute path on Python 2. __file__ = os.path.abspath(__file__) diff --git a/src/debugpy/server/__init__.py b/src/debugpy/server/__init__.py index e6a1ad660..5f29a87a1 100644 --- a/src/debugpy/server/__init__.py +++ b/src/debugpy/server/__init__.py @@ -4,6 +4,50 @@ from __future__ import absolute_import, division, print_function, unicode_literals +from importlib import import_module +import os + # "force_pydevd" must be imported first to ensure (via side effects) # that the debugpy-vendored copy of pydevd gets used. -import debugpy._vendored.force_pydevd # noqa +import debugpy +if debugpy.__bundling_disabled__: + # Do what force_pydevd.py does, but using the system-provided + # pydevd. + + # XXX: This is copied here so that the whole '_vendored' directory + # can be deleted when DEBUGPY_BUNDLING_DISABLED is set. + + # If debugpy logging is enabled, enable it for pydevd as well + if "DEBUGPY_LOG_DIR" in os.environ: + os.environ[str("PYDEVD_DEBUG")] = str("True") + os.environ[str("PYDEVD_DEBUG_FILE")] = \ + os.environ["DEBUGPY_LOG_DIR"] + str("/debugpy.pydevd.log") + + # Work around https://github.com/microsoft/debugpy/issues/346. + # Disable pydevd frame-eval optimizations only if unset, to allow opt-in. + if "PYDEVD_USE_FRAME_EVAL" not in os.environ: + os.environ[str("PYDEVD_USE_FRAME_EVAL")] = str("NO") + + # Constants must be set before importing any other pydevd module + # due to heavy use of "from" in them. + pydevd_constants = import_module('_pydevd_bundle.pydevd_constants') + # The default pydevd value is 1000. + pydevd_constants.MAXIMUM_VARIABLE_REPRESENTATION_SIZE = 2 ** 32 + + # When pydevd is imported it sets the breakpoint behavior, but it needs to be + # overridden because by default pydevd will connect to the remote debugger using + # its own custom protocol rather than DAP. + import pydevd # noqa + import debugpy # noqa + + def debugpy_breakpointhook(): + debugpy.breakpoint() + + pydevd.install_breakpointhook(debugpy_breakpointhook) + + # Ensure that pydevd uses JSON protocol + from _pydevd_bundle import pydevd_constants + from _pydevd_bundle import pydevd_defaults + pydevd_defaults.PydevdCustomization.DEFAULT_PROTOCOL = pydevd_constants.HTTP_JSON_PROTOCOL +else: + import debugpy._vendored.force_pydevd # noqa diff --git a/src/debugpy/server/attach_pid_injected.py b/src/debugpy/server/attach_pid_injected.py index e63459968..87cfdd53b 100644 --- a/src/debugpy/server/attach_pid_injected.py +++ b/src/debugpy/server/attach_pid_injected.py @@ -8,6 +8,7 @@ import os +import debugpy __file__ = os.path.abspath(__file__) _debugpy_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) @@ -30,25 +31,29 @@ def on_exception(msg): def on_critical(msg): print(msg, file=sys.stderr) - pydevd_attach_to_process_path = os.path.join( - _debugpy_dir, - "debugpy", - "_vendored", - "pydevd", - "pydevd_attach_to_process", - ) - assert os.path.exists(pydevd_attach_to_process_path) - sys.path.insert(0, pydevd_attach_to_process_path) - - # NOTE: that it's not a part of the pydevd PYTHONPATH - import attach_script + if debugpy.__bundling_disabled__: + from pydevd_attach_to_process import attach_script + else: + pydevd_attach_to_process_path = os.path.join( + _debugpy_dir, + "debugpy", + "_vendored", + "pydevd", + "pydevd_attach_to_process", + ) + assert os.path.exists(pydevd_attach_to_process_path) + sys.path.insert(0, pydevd_attach_to_process_path) + + # NOTE: that it's not a part of the pydevd PYTHONPATH + import attach_script attach_script.fix_main_thread_id( on_warn=on_warn, on_exception=on_exception, on_critical=on_critical ) - # NOTE: At this point it should be safe to remove this. - sys.path.remove(pydevd_attach_to_process_path) + if not debugpy.__bundling_disabled__: + # NOTE: At this point it should be safe to remove this. + sys.path.remove(pydevd_attach_to_process_path) except: import traceback diff --git a/tests/tests/test_vendoring.py b/tests/tests/test_vendoring.py index dd6c4269b..28c03702d 100644 --- a/tests/tests/test_vendoring.py +++ b/tests/tests/test_vendoring.py @@ -1,3 +1,8 @@ +import pytest + +import debugpy + +@pytest.mark.skipif(debugpy.__bundling_disabled__, reason='Bundling disabled') def test_vendoring(pyfile): @pyfile def import_debugpy():