From 6d049b73abe4e74b6a5c782a081cd0115b8499cc Mon Sep 17 00:00:00 2001 From: Fabio Zadrozny Date: Thu, 2 Jun 2022 16:01:35 -0300 Subject: [PATCH] Auto-attach to subprocess with .zip targets. Fixes #945 --- .../pydevd/_pydev_bundle/pydev_monkey.py | 14 +- .../_pydevd_bundle/pydevd_dont_trace_files.py | 1 + .../pydevd/_pydevd_bundle/pydevd_runpy.py | 353 ++++++++++++++++++ .../pydevd/build_tools/generate_code.py | 4 +- .../_vendored/pydevd/pydev_run_in_console.py | 13 +- src/debugpy/_vendored/pydevd/pydevd.py | 13 +- .../pydevd/tests_python/debugger_unittest.py | 3 +- .../resources/_debugger_case_check_tracer.py | 52 +-- .../_debugger_case_subprocess_target.zip | Bin 0 -> 177 bytes .../_debugger_case_subprocess_zip.py | 34 ++ .../pydevd/tests_python/test_debugger.py | 9 +- src/debugpy/server/cli.py | 4 +- 12 files changed, 434 insertions(+), 66 deletions(-) create mode 100644 src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py create mode 100644 src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_subprocess_target.zip create mode 100644 src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_subprocess_zip.py diff --git a/src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_monkey.py b/src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_monkey.py index d998a5bc8..150718f08 100644 --- a/src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_monkey.py +++ b/src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_monkey.py @@ -487,14 +487,14 @@ def patch_args(args, is_exec=False): # It doesn't start with '-' and we didn't ignore this entry: # this means that this is the file to be executed. filename = unquoted_args[i] - filename_i = i - # When executing .zip applications, don't attach the debugger. - extensions = _get_str_type_compatible(filename, ['.zip', '.pyz', '.pyzw']) - for ext in extensions: - if filename.endswith(ext): - pydev_log.debug('Executing a PyZip (debugger will not be attached to subprocess).') - return original_args + # Note that the filename is not validated here. + # There are cases where even a .exe is valid (xonsh.exe): + # https://github.com/microsoft/debugpy/issues/945 + # So, we should support whatever runpy.run_path + # supports in this case. + + filename_i = i if _is_managed_arg(filename): # no need to add pydevd twice pydev_log.debug('Skipped monkey-patching as pydevd.py is in args already.') diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_dont_trace_files.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_dont_trace_files.py index e95446d55..d73765399 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_dont_trace_files.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_dont_trace_files.py @@ -121,6 +121,7 @@ 'pydevd_referrers.py': PYDEV_FILE, 'pydevd_reload.py': PYDEV_FILE, 'pydevd_resolver.py': PYDEV_FILE, + 'pydevd_runpy.py': PYDEV_FILE, 'pydevd_safe_repr.py': PYDEV_FILE, 'pydevd_save_locals.py': PYDEV_FILE, 'pydevd_schema.py': PYDEV_FILE, diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py new file mode 100644 index 000000000..09b713f06 --- /dev/null +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py @@ -0,0 +1,353 @@ +""" +Vendored copy of runpy from the standard library. + +It's vendored so that we can properly ignore it when used to start user code +while still making it possible for the user to debug runpy itself. + +runpy.py - locating and running Python code using the module namespace + +Provides support for locating and running Python scripts using the Python +module namespace instead of the native filesystem. + +This allows Python code to play nicely with non-filesystem based PEP 302 +importers when locating support scripts as well as when importing modules. +""" +# Written by Nick Coghlan +# to implement PEP 338 (Executing Modules as Scripts) + +import sys +import importlib.machinery # importlib first so we can test #15386 via -m +import importlib.util +import io +import types +import os + +__all__ = [ + "run_module", "run_path", +] + + +# Note: fabioz: Don't use pkgutil (when handling caught exceptions we could end up +# showing exceptions in pkgutil.get_imported (specifically the KeyError), so, +# create a copy of the function we need to properly ignore this exception when +# running the program. +def pkgutil_get_importer(path_item): + """Retrieve a finder for the given path item + + The returned finder is cached in sys.path_importer_cache + if it was newly created by a path hook. + + The cache (or part of it) can be cleared manually if a + rescan of sys.path_hooks is necessary. + """ + try: + importer = sys.path_importer_cache[path_item] + except KeyError: + for path_hook in sys.path_hooks: + try: + importer = path_hook(path_item) + sys.path_importer_cache.setdefault(path_item, importer) + break + except ImportError: + pass + else: + importer = None + return importer + + +class _TempModule(object): + """Temporarily replace a module in sys.modules with an empty namespace""" + + def __init__(self, mod_name): + self.mod_name = mod_name + self.module = types.ModuleType(mod_name) + self._saved_module = [] + + def __enter__(self): + mod_name = self.mod_name + try: + self._saved_module.append(sys.modules[mod_name]) + except KeyError: + pass + sys.modules[mod_name] = self.module + return self + + def __exit__(self, *args): + if self._saved_module: + sys.modules[self.mod_name] = self._saved_module[0] + else: + del sys.modules[self.mod_name] + self._saved_module = [] + + +class _ModifiedArgv0(object): + + def __init__(self, value): + self.value = value + self._saved_value = self._sentinel = object() + + def __enter__(self): + if self._saved_value is not self._sentinel: + raise RuntimeError("Already preserving saved value") + self._saved_value = sys.argv[0] + sys.argv[0] = self.value + + def __exit__(self, *args): + self.value = self._sentinel + sys.argv[0] = self._saved_value + + +# TODO: Replace these helpers with importlib._bootstrap_external functions. +def _run_code(code, run_globals, init_globals=None, + mod_name=None, mod_spec=None, + pkg_name=None, script_name=None): + """Helper to run code in nominated namespace""" + if init_globals is not None: + run_globals.update(init_globals) + if mod_spec is None: + loader = None + fname = script_name + cached = None + else: + loader = mod_spec.loader + fname = mod_spec.origin + cached = mod_spec.cached + if pkg_name is None: + pkg_name = mod_spec.parent + run_globals.update(__name__=mod_name, + __file__=fname, + __cached__=cached, + __doc__=None, + __loader__=loader, + __package__=pkg_name, + __spec__=mod_spec) + exec(code, run_globals) + return run_globals + + +def _run_module_code(code, init_globals=None, + mod_name=None, mod_spec=None, + pkg_name=None, script_name=None): + """Helper to run code in new namespace with sys modified""" + fname = script_name if mod_spec is None else mod_spec.origin + with _TempModule(mod_name) as temp_module, _ModifiedArgv0(fname): + mod_globals = temp_module.module.__dict__ + _run_code(code, mod_globals, init_globals, + mod_name, mod_spec, pkg_name, script_name) + # Copy the globals of the temporary module, as they + # may be cleared when the temporary module goes away + return mod_globals.copy() + + +# Helper to get the full name, spec and code for a module +def _get_module_details(mod_name, error=ImportError): + if mod_name.startswith("."): + raise error("Relative module names not supported") + pkg_name, _, _ = mod_name.rpartition(".") + if pkg_name: + # Try importing the parent to avoid catching initialization errors + try: + __import__(pkg_name) + except ImportError as e: + # If the parent or higher ancestor package is missing, let the + # error be raised by find_spec() below and then be caught. But do + # not allow other errors to be caught. + if e.name is None or (e.name != pkg_name and + not pkg_name.startswith(e.name + ".")): + raise + # Warn if the module has already been imported under its normal name + existing = sys.modules.get(mod_name) + if existing is not None and not hasattr(existing, "__path__"): + from warnings import warn + msg = "{mod_name!r} found in sys.modules after import of " \ + "package {pkg_name!r}, but prior to execution of " \ + "{mod_name!r}; this may result in unpredictable " \ + "behaviour".format(mod_name=mod_name, pkg_name=pkg_name) + warn(RuntimeWarning(msg)) + + try: + spec = importlib.util.find_spec(mod_name) + except (ImportError, AttributeError, TypeError, ValueError) as ex: + # This hack fixes an impedance mismatch between pkgutil and + # importlib, where the latter raises other errors for cases where + # pkgutil previously raised ImportError + msg = "Error while finding module specification for {!r} ({}: {})" + if mod_name.endswith(".py"): + msg += (f". Try using '{mod_name[:-3]}' instead of " + f"'{mod_name}' as the module name.") + raise error(msg.format(mod_name, type(ex).__name__, ex)) from ex + if spec is None: + raise error("No module named %s" % mod_name) + if spec.submodule_search_locations is not None: + if mod_name == "__main__" or mod_name.endswith(".__main__"): + raise error("Cannot use package as __main__ module") + try: + pkg_main_name = mod_name + ".__main__" + return _get_module_details(pkg_main_name, error) + except error as e: + if mod_name not in sys.modules: + raise # No module loaded; being a package is irrelevant + raise error(("%s; %r is a package and cannot " + + "be directly executed") % (e, mod_name)) + loader = spec.loader + if loader is None: + raise error("%r is a namespace package and cannot be executed" + % mod_name) + try: + code = loader.get_code(mod_name) + except ImportError as e: + raise error(format(e)) from e + if code is None: + raise error("No code object available for %s" % mod_name) + return mod_name, spec, code + + +class _Error(Exception): + """Error that _run_module_as_main() should report without a traceback""" + + +# XXX ncoghlan: Should this be documented and made public? +# (Current thoughts: don't repeat the mistake that lead to its +# creation when run_module() no longer met the needs of +# mainmodule.c, but couldn't be changed because it was public) +def _run_module_as_main(mod_name, alter_argv=True): + """Runs the designated module in the __main__ namespace + + Note that the executed module will have full access to the + __main__ namespace. If this is not desirable, the run_module() + function should be used to run the module code in a fresh namespace. + + At the very least, these variables in __main__ will be overwritten: + __name__ + __file__ + __cached__ + __loader__ + __package__ + """ + try: + if alter_argv or mod_name != "__main__": # i.e. -m switch + mod_name, mod_spec, code = _get_module_details(mod_name, _Error) + else: # i.e. directory or zipfile execution + mod_name, mod_spec, code = _get_main_module_details(_Error) + except _Error as exc: + msg = "%s: %s" % (sys.executable, exc) + sys.exit(msg) + main_globals = sys.modules["__main__"].__dict__ + if alter_argv: + sys.argv[0] = mod_spec.origin + return _run_code(code, main_globals, None, + "__main__", mod_spec) + + +def run_module(mod_name, init_globals=None, + run_name=None, alter_sys=False): + """Execute a module's code without importing it + + Returns the resulting top level namespace dictionary + """ + mod_name, mod_spec, code = _get_module_details(mod_name) + if run_name is None: + run_name = mod_name + if alter_sys: + return _run_module_code(code, init_globals, run_name, mod_spec) + else: + # Leave the sys module alone + return _run_code(code, {}, init_globals, run_name, mod_spec) + + +def _get_main_module_details(error=ImportError): + # Helper that gives a nicer error message when attempting to + # execute a zipfile or directory by invoking __main__.py + # Also moves the standard __main__ out of the way so that the + # preexisting __loader__ entry doesn't cause issues + main_name = "__main__" + saved_main = sys.modules[main_name] + del sys.modules[main_name] + try: + return _get_module_details(main_name) + except ImportError as exc: + if main_name in str(exc): + raise error("can't find %r module in %r" % + (main_name, sys.path[0])) from exc + raise + finally: + sys.modules[main_name] = saved_main + + +try: + io_open_code = io.open_code +except AttributeError: + # Compatibility with Python 3.6/3.7 + import tokenize + io_open_code = tokenize.open + + +def _get_code_from_file(run_name, fname): + # Check for a compiled file first + from pkgutil import read_code + decoded_path = os.path.abspath(os.fsdecode(fname)) + with io_open_code(decoded_path) as f: + code = read_code(f) + if code is None: + # That didn't work, so try it as normal source code + with io_open_code(decoded_path) as f: + code = compile(f.read(), fname, 'exec') + return code, fname + + +def run_path(path_name, init_globals=None, run_name=None): + """Execute code located at the specified filesystem location + + Returns the resulting top level namespace dictionary + + The file path may refer directly to a Python script (i.e. + one that could be directly executed with execfile) or else + it may refer to a zipfile or directory containing a top + level __main__.py script. + """ + if run_name is None: + run_name = "" + pkg_name = run_name.rpartition(".")[0] + importer = pkgutil_get_importer(path_name) + # Trying to avoid importing imp so as to not consume the deprecation warning. + is_NullImporter = False + if type(importer).__module__ == 'imp': + if type(importer).__name__ == 'NullImporter': + is_NullImporter = True + if isinstance(importer, type(None)) or is_NullImporter: + # Not a valid sys.path entry, so run the code directly + # execfile() doesn't help as we want to allow compiled files + code, fname = _get_code_from_file(run_name, path_name) + return _run_module_code(code, init_globals, run_name, + pkg_name=pkg_name, script_name=fname) + else: + # Finder is defined for path, so add it to + # the start of sys.path + sys.path.insert(0, path_name) + try: + # Here's where things are a little different from the run_module + # case. There, we only had to replace the module in sys while the + # code was running and doing so was somewhat optional. Here, we + # have no choice and we have to remove it even while we read the + # code. If we don't do this, a __loader__ attribute in the + # existing __main__ module may prevent location of the new module. + mod_name, mod_spec, code = _get_main_module_details() + with _TempModule(run_name) as temp_module, \ + _ModifiedArgv0(path_name): + mod_globals = temp_module.module.__dict__ + return _run_code(code, mod_globals, init_globals, + run_name, mod_spec, pkg_name).copy() + finally: + try: + sys.path.remove(path_name) + except ValueError: + pass + + +if __name__ == "__main__": + # Run the module specified as the next command line argument + if len(sys.argv) < 2: + print("No module specified for execution", file=sys.stderr) + else: + del sys.argv[0] # Make the requested module sys.argv[0] + _run_module_as_main(sys.argv[0]) diff --git a/src/debugpy/_vendored/pydevd/build_tools/generate_code.py b/src/debugpy/_vendored/pydevd/build_tools/generate_code.py index 98842155e..ccdd399ac 100644 --- a/src/debugpy/_vendored/pydevd/build_tools/generate_code.py +++ b/src/debugpy/_vendored/pydevd/build_tools/generate_code.py @@ -188,8 +188,8 @@ def generate_dont_trace_files(): pydev_files.append(" '%s': PYDEV_FILE," % (f,)) contents = template % (dict( - pydev_files='\n'.join(sorted(pydev_files)), - pydev_dirs='\n'.join(sorted(pydev_dirs)), + pydev_files='\n'.join(sorted(set(pydev_files))), + pydev_dirs='\n'.join(sorted(set(pydev_dirs))), )) assert 'pydevd.py' in contents assert 'pydevd_dont_trace.py' in contents diff --git a/src/debugpy/_vendored/pydevd/pydev_run_in_console.py b/src/debugpy/_vendored/pydevd/pydev_run_in_console.py index cf9aa7b1c..a87a0e4b3 100644 --- a/src/debugpy/_vendored/pydevd/pydev_run_in_console.py +++ b/src/debugpy/_vendored/pydevd/pydev_run_in_console.py @@ -17,7 +17,7 @@ def run_file(file, globals=None, locals=None, is_module=False): module_name = None entry_point_fn = None if is_module: - file, _, entry_point_fn = file.partition(':') + file, _, entry_point_fn = file.partition(':') module_name = file filename = get_fullname(file) if filename is None: @@ -60,11 +60,8 @@ def run_file(file, globals=None, locals=None, is_module=False): func() else: # Run with the -m switch - import runpy - if hasattr(runpy, '_run_module_as_main'): - runpy._run_module_as_main(module_name) - else: - runpy.run_module(module_name) + from _pydevd_bundle import pydevd_runpy + pydevd_runpy._run_module_as_main(module_name) except: traceback.print_exc() @@ -121,8 +118,8 @@ def process_args(argv): host = pydev_localhost.get_localhost() - #replace exit (see comments on method) - #note that this does not work in jython!!! (sys method can't be replaced). + # replace exit (see comments on method) + # note that this does not work in jython!!! (sys method can't be replaced). sys.exit = skip_successful_exit connect_status_queue = _queue.Queue() diff --git a/src/debugpy/_vendored/pydevd/pydevd.py b/src/debugpy/_vendored/pydevd/pydevd.py index 367b6d84d..951053603 100644 --- a/src/debugpy/_vendored/pydevd/pydevd.py +++ b/src/debugpy/_vendored/pydevd/pydevd.py @@ -43,6 +43,7 @@ from _pydevd_bundle.pydevd_filtering import FilesFiltering, glob_matches_path from _pydevd_bundle import pydevd_io, pydevd_vm_type from _pydevd_bundle import pydevd_utils +from _pydevd_bundle import pydevd_runpy from _pydev_bundle.pydev_console_utils import DebugConsoleStdIn from _pydevd_bundle.pydevd_additional_thread_info import set_additional_thread_info from _pydevd_bundle.pydevd_breakpoints import ExceptionBreakpoint, get_exception_breakpoint @@ -2481,7 +2482,7 @@ def _exec(self, is_module, entry_point_fn, module_name, file, globals, locals): This function should have frames tracked by unhandled exceptions (the `_exec` name is important). ''' if not is_module: - pydev_imports.execfile(file, globals, locals) # execute the script + globals = pydevd_runpy.run_path(file, globals, '__main__') else: # treat ':' as a separator between module and entry point function # if there is no entry point we run we same as with -m switch. Otherwise we perform @@ -2492,15 +2493,7 @@ def _exec(self, is_module, entry_point_fn, module_name, file, globals, locals): func() else: # Run with the -m switch - import runpy - if hasattr(runpy, '_run_module_as_main'): - # Newer versions of Python actually use this when the -m switch is used. - if sys.version_info[:2] <= (2, 6): - runpy._run_module_as_main(module_name, set_argv0=False) - else: - runpy._run_module_as_main(module_name, alter_argv=False) - else: - runpy.run_module(module_name) + globals = pydevd_runpy._run_module_as_main(module_name, alter_argv=False) return globals def wait_for_commands(self, globals): diff --git a/src/debugpy/_vendored/pydevd/tests_python/debugger_unittest.py b/src/debugpy/_vendored/pydevd/tests_python/debugger_unittest.py index f8f8425e9..34167d6b1 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/debugger_unittest.py +++ b/src/debugpy/_vendored/pydevd/tests_python/debugger_unittest.py @@ -1391,7 +1391,8 @@ def wait_for_thread_join(self, main_thread_id): def condition(): return self.get_frame_names(main_thread_id) in ( ['wait', 'join', ''], - ['_wait_for_tstate_lock', 'join', ''] + ['_wait_for_tstate_lock', 'join', ''], + ['_wait_for_tstate_lock', 'join', '', '_run_code', '_run_module_code', 'run_path'], ) def msg(): diff --git a/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_check_tracer.py b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_check_tracer.py index 0b2847666..f0b0d35da 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_check_tracer.py +++ b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_check_tracer.py @@ -40,7 +40,7 @@ def fail(): for found_info, expected_info in zip(found, expected): if found_info.filename != expected_info.filename or found_info.name != expected_info.name: fail() - + for f_trace in expected_info.f_trace.split('|'): if f_trace == found_info.f_trace: break @@ -49,22 +49,13 @@ def fail(): def thread_func(): - if sys.version_info[0] >= 3: - check_frame_info([ - FrameInfo(filename='_debugger_case_check_tracer.py', name='thread_func', f_trace='trace_exception'), - FrameInfo(filename='threading.py', name='run', f_trace='None'), - FrameInfo(filename='threading.py', name='_bootstrap_inner', f_trace='trace_unhandled_exceptions'), - FrameInfo(filename='threading.py', name='_bootstrap', f_trace='None'), - FrameInfo(filename='pydev_monkey.py', name='__call__', f_trace='None') - ]) - else: - check_frame_info([ - FrameInfo(filename='_debugger_case_check_tracer.py', name='thread_func', f_trace='trace_exception'), - FrameInfo(filename='threading.py', name='run', f_trace='None'), - FrameInfo(filename='threading.py', name='__bootstrap_inner', f_trace='trace_unhandled_exceptions'), - FrameInfo(filename='threading.py', name='__bootstrap', f_trace='None'), - FrameInfo(filename='pydev_monkey.py', name='__call__', f_trace='None'), - ]) + check_frame_info([ + FrameInfo(filename='_debugger_case_check_tracer.py', name='thread_func', f_trace='trace_exception'), + FrameInfo(filename='threading.py', name='run', f_trace='None'), + FrameInfo(filename='threading.py', name='_bootstrap_inner', f_trace='trace_unhandled_exceptions'), + FrameInfo(filename='threading.py', name='_bootstrap', f_trace='None'), + FrameInfo(filename='pydev_monkey.py', name='__call__', f_trace='None') + ]) th = threading.Thread(target=thread_func) @@ -95,22 +86,15 @@ def thread_func2(): import time time.sleep(.3) -if sys.version_info[0] >= 3: - check_frame_info([ - FrameInfo(filename='_debugger_case_check_tracer.py', name='', f_trace='trace_exception'), - FrameInfo(filename='_pydev_execfile.py', name='execfile', f_trace='None'), - FrameInfo(filename='pydevd.py', name='_exec', f_trace='trace_unhandled_exceptions'), - FrameInfo(filename='pydevd.py', name='run', f_trace='trace_dispatch|None'), - FrameInfo(filename='pydevd.py', name='main', f_trace='trace_dispatch|None'), - FrameInfo(filename='pydevd.py', name='', f_trace='trace_dispatch|None') - ]) -else: - check_frame_info([ - FrameInfo(filename='_debugger_case_check_tracer.py', name='', f_trace='trace_exception'), - FrameInfo(filename='pydevd.py', name='_exec', f_trace='trace_unhandled_exceptions'), - FrameInfo(filename='pydevd.py', name='run', f_trace='trace_dispatch|None'), - FrameInfo(filename='pydevd.py', name='main', f_trace='trace_dispatch|None'), - FrameInfo(filename='pydevd.py', name='', f_trace='trace_dispatch|None'), - ]) +check_frame_info([ + FrameInfo(filename='_debugger_case_check_tracer.py', name='', f_trace='trace_exception'), + FrameInfo(filename='pydevd_runpy.py', name='_run_code', f_trace='None'), + FrameInfo(filename='pydevd_runpy.py', name='_run_module_code', f_trace='None'), + FrameInfo(filename='pydevd_runpy.py', name='run_path', f_trace='None'), + FrameInfo(filename='pydevd.py', name='_exec', f_trace='trace_unhandled_exceptions'), + FrameInfo(filename='pydevd.py', name='run', f_trace='trace_dispatch|None'), + FrameInfo(filename='pydevd.py', name='main', f_trace='trace_dispatch|None'), + FrameInfo(filename='pydevd.py', name='', f_trace='trace_dispatch|None') +]) print('TEST SUCEEDED') diff --git a/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_subprocess_target.zip b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_subprocess_target.zip new file mode 100644 index 0000000000000000000000000000000000000000..d9f9c61df2e84f35fa1f0cf04e683c23012f4601 GIT binary patch literal 177 zcmWIWW@Zs#U|`^2=&LytvSg<2OG_Xx1ch-NZrSssB_Uh?#W=s~IA2ww^_oK{=B1VP)Z$>5& f2HbW4tpS4uMi7N$V}Lg+8%T%|2tkfY0&y4s=ua&F literal 0 HcmV?d00001 diff --git a/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_subprocess_zip.py b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_subprocess_zip.py new file mode 100644 index 000000000..e6a26fc3e --- /dev/null +++ b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_subprocess_zip.py @@ -0,0 +1,34 @@ +import subprocess +import sys +import os + +target_zip = os.path.join(os.path.dirname(__file__), '_debugger_case_subprocess_target.zip') + + +def create_zip(): + import zipfile + z = zipfile.ZipFile(target_zip, 'w', zipfile.ZIP_DEFLATED) + z.writestr('__main__.py', ''' +import _debugger_case_subprocess_zip +_debugger_case_subprocess_zip.call_from_zip() +''') + + +def call_from_zip(): + print('Called from zip') # break here + + +def main(): + env = os.environ.copy() + env['PYTHONPATH'] = os.path.dirname(__file__) + result = subprocess.check_output([sys.executable, target_zip], env=env).decode('utf-8') + assert 'Called from zip' in result + print('TEST SUCEEDED!') + + +if __name__ == '__main__': + if not os.path.exists(target_zip): + create_zip() + assert os.path.exists(target_zip) + + main() diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py b/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py index 693abf300..6b6881e92 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py +++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py @@ -2667,10 +2667,13 @@ def run(self): @pytest.mark.skipif(not IS_CPYTHON, reason='CPython only test.') -def test_subprocess_quoted_args(case_setup_multiprocessing): - import threading +@pytest.mark.parametrize('target', [ + '_debugger_case_quoting.py', + '_debugger_case_subprocess_zip.py' +]) +def test_subprocess_quoted_args(case_setup_multiprocessing, target): from tests_python.debugger_unittest import AbstractWriterThread - with case_setup_multiprocessing.test_file('_debugger_case_quoting.py') as writer: + with case_setup_multiprocessing.test_file(target) as writer: break_subprocess_line = writer.get_line_index_with_content('break here') writer.write_add_breakpoint(break_subprocess_line) diff --git a/src/debugpy/server/cli.py b/src/debugpy/server/cli.py index 05b1bebd4..8a2a57803 100644 --- a/src/debugpy/server/cli.py +++ b/src/debugpy/server/cli.py @@ -5,7 +5,6 @@ import json import os import re -import runpy import sys from importlib.util import find_spec @@ -15,6 +14,9 @@ assert "pydevd" in sys.modules import pydevd +# Note: use the one bundled from pydevd so that it's invisible for the user. +from _pydevd_bundle import pydevd_runpy as runpy + import debugpy from debugpy.common import log from debugpy.server import api