From 9543d567ecc0064ffc0d5ea6d2d91e0196105aa6 Mon Sep 17 00:00:00 2001 From: Nicolas Graves Date: Fri, 17 May 2024 01:42:33 +0200 Subject: [PATCH] Allow using pydevd as a regular dependency. --- setup.py | 54 +++++++++++-------- src/debugpy/__init__.py | 7 +++ src/debugpy/_vendored/force_pydevd.py | 43 +-------------- src/debugpy/server/__init__.py | 66 ++++++++++++++++++++++- src/debugpy/server/attach_pid_injected.py | 33 +++++++----- tests/tests/test_vendoring.py | 6 +++ 6 files changed, 131 insertions(+), 78 deletions(-) diff --git a/setup.py b/setup.py index 1bfba237b..afffc6b21 100644 --- a/setup.py +++ b/setup.py @@ -11,6 +11,8 @@ import sys +BUNDLE_DEBUGPY = not (os.getenv("BUNDLE_DEBUGPY").strip().lower() == "0") + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import versioneer # noqa @@ -18,12 +20,14 @@ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src")) import debugpy -import debugpy._vendored -del sys.path[0] +if BUNDLE_DEBUGPY: + import debugpy._vendored +del sys.path[0] -PYDEVD_ROOT = debugpy._vendored.project_root("pydevd") +if BUNDLE_DEBUGPY: + PYDEVD_ROOT = debugpy._vendored.project_root("pydevd") DEBUGBY_ROOT = os.path.dirname(os.path.abspath(debugpy.__file__)) @@ -46,7 +50,7 @@ def get_buildplatform(): # relevant setuptools versions. class ExtModules(list): def __bool__(self): - return True + return BUNDLE_DEBUGPY def override_build(cmds): @@ -147,7 +151,28 @@ def tail_is(*suffixes): cmds = versioneer.get_cmdclass() override_build(cmds) - override_build_py(cmds) + if BUNDLE_DEBUGPY: + override_build_py(cmds) + + data = {"debugpy": ["ThirdPartyNotices.txt"]} + packages = [ + "debugpy", + "debugpy.adapter", + "debugpy.common", + "debugpy.launcher", + "debugpy.server", + ] + if BUNDLE_DEBUGPY: + data.update( + { + "debugpy._vendored": [ + # pydevd extensions must be built before this list can + # be computed properly, so it is populated in the + # overridden build_py.finalize_options(). + ] + } + ) + packages.append("debugpy._vendored") setuptools.setup( name="debugpy", @@ -177,23 +202,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": [ - # pydevd extensions must be built before this list can be computed properly, - # so it is populated in the overridden build_py.finalize_options(). - ], - }, + packages=packages, + package_data=data, ext_modules=ExtModules(), - has_ext_modules=lambda: True, + has_ext_modules=lambda: BUNDLE_DEBUGPY, cmdclass=cmds, **extras ) diff --git a/src/debugpy/__init__.py b/src/debugpy/__init__.py index 975bec79b..512219d70 100644 --- a/src/debugpy/__init__.py +++ b/src/debugpy/__init__.py @@ -24,6 +24,13 @@ import sys +try: + import debugpy._vendored # noqa + + is_pydevd_bundled = True +except ImportError: + is_pydevd_bundled = False + assert sys.version_info >= (3, 7), ( "Python 3.6 and below is not supported by this version of debugpy; " "use debugpy 1.5.1 or earlier." diff --git a/src/debugpy/_vendored/force_pydevd.py b/src/debugpy/_vendored/force_pydevd.py index cfd892758..99622f8ce 100644 --- a/src/debugpy/_vendored/force_pydevd.py +++ b/src/debugpy/_vendored/force_pydevd.py @@ -3,7 +3,6 @@ # for license information. from importlib import import_module -import os import warnings from . import check_modules, prefix_matcher, preimport, vendored @@ -17,23 +16,12 @@ # raise ImportError(msg) warnings.warn(msg + ':\n {}'.format('\n '.join(_unvendored))) -# 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") - -# 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. +# due to heavy use of "from" in them. with warnings.catch_warnings(): warnings.simplefilter("ignore", category=DeprecationWarning) with vendored('pydevd'): pydevd_constants = import_module('_pydevd_bundle.pydevd_constants') -# We limit representation size in our representation provider when needed. -pydevd_constants.MAXIMUM_VARIABLE_REPRESENTATION_SIZE = 2 ** 32 # Now make sure all the top-level modules and packages in pydevd are # loaded. Any pydevd modules that aren't loaded at this point, will @@ -50,32 +38,3 @@ 'pydevd_plugins', 'pydevd', ]) - -# 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 - -# Enable some defaults related to debugpy such as sending a single notification when -# threads pause and stopping on any exception. -pydevd_defaults.PydevdCustomization.DEBUG_MODE = 'debugpy-dap' - -# This is important when pydevd attaches automatically to a subprocess. In this case, we have to -# make sure that debugpy is properly put back in the game for users to be able to use it. -pydevd_defaults.PydevdCustomization.PREIMPORT = '%s;%s' % ( - os.path.dirname(os.path.dirname(debugpy.__file__)), - 'debugpy._vendored.force_pydevd' -) diff --git a/src/debugpy/server/__init__.py b/src/debugpy/server/__init__.py index 42d5367f0..a27c6bf42 100644 --- a/src/debugpy/server/__init__.py +++ b/src/debugpy/server/__init__.py @@ -2,6 +2,70 @@ # Licensed under the MIT License. See LICENSE in the project root # for license information. +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 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" + ) + +# 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") + +BUNDLE_DEBUGPY = bool(os.getenv("BUNDLE_DEBUGPY")) + +# Constants must be set before importing any other pydevd module +# due to heavy use of "from" in them. +if BUNDLE_DEBUGPY: + try: + import debugpy._vendored.force_pydevd # noqa + except Exception as e: + raise e +else: + pydevd_constants = import_module("_pydevd_bundle.pydevd_constants") + +# We limit representation size in our representation provider when needed. +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 +) + +# Enable some defaults related to debugpy such as sending a single notification when +# threads pause and stopping on any exception. +pydevd_defaults.PydevdCustomization.DEBUG_MODE = "debugpy-dap" + +# This is important when pydevd attaches automatically to a subprocess. In this case, we have to +# make sure that debugpy is properly put back in the game for users to be able to use it. +if not BUNDLE_DEBUGPY: + pydevd_defaults.PydevdCustomization.PREIMPORT = "%s;%s" % ( + os.path.dirname(os.path.dirname(debugpy.__file__)), + "debugpy._vendored.force_pydevd", + ) diff --git a/src/debugpy/server/attach_pid_injected.py b/src/debugpy/server/attach_pid_injected.py index a8df6e1e2..06e49600f 100644 --- a/src/debugpy/server/attach_pid_injected.py +++ b/src/debugpy/server/attach_pid_injected.py @@ -6,6 +6,7 @@ import os +import debugpy __file__ = os.path.abspath(__file__) _debugpy_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) @@ -28,25 +29,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 not debugpy.is_pydevd_bundled: + 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 debugpy.is_pydevd_bundled: + # 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 9cf44220c..3188eb9e0 100644 --- a/tests/tests/test_vendoring.py +++ b/tests/tests/test_vendoring.py @@ -3,6 +3,12 @@ # for license information. +import pytest + +import debugpy + + +@pytest.mark.skipif(not debugpy.is_pydevd_bundled, reason="pydevd is not bundled") def test_vendoring(pyfile): @pyfile def import_debugpy():