diff --git a/.flake8 b/.flake8 index b1da661b..92506af4 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,8 @@ [flake8] ignore = W, E24,E121,E123,E125,E126,E221,E226,E266,E704, - E265,E722,E501,E731,E306,E401,E302,E222,E303 + E265,E722,E501,E731,E306,E401,E302,E222,E303, + E402 exclude = ptvsd/_vendored/pydevd, ./.eggs, diff --git a/ptvsd/__main__.py b/ptvsd/__main__.py index 745ca817..8c75f528 100644 --- a/ptvsd/__main__.py +++ b/ptvsd/__main__.py @@ -2,10 +2,37 @@ # Licensed under the MIT License. See LICENSE in the project root # for license information. +from __future__ import print_function, with_statement, absolute_import + import argparse import os.path import sys + +# ptvsd can also be invoked directly rather than via -m. In this case, the +# first entry on sys.path is the one added automatically by Python for the +# directory containing this file. This means that 1) import ptvsd will not +# work, since we need the parent directory of ptvsd/ to be on path, rather +# than ptvsd/ itself, and 2) many other absolute imports will break, because +# they will be resolved relative to ptvsd/ - e.g. import socket will then +# try to import ptvsd/socket.py! +# +# To fix this, we need to replace the automatically added entry such that it +# points at the parent directory instead, import ptvsd from that directory, +# and then remove than entry altogether so that it doesn't affect any further +# imports. For example, suppose the user did: +# +# python /foo/bar/ptvsd ... +# +# At the beginning of this script, sys.path will contain '/foo/bar/ptvsd' as +# the first entry. What we want is to replace it with '/foo/bar', then import +# ptvsd with that in effect, and then remove it before continuing execution. +if __name__ == '__main__' and 'ptvsd' not in sys.modules: + sys.path[0] = os.path.dirname(sys.path[0]) + import ptvsd # noqa + del sys.path[0] + + from ptvsd import multiproc, options from ptvsd._attach import attach_main from ptvsd._local import debug_main, run_main @@ -13,6 +40,17 @@ from ptvsd.socket import Address from ptvsd.version import __version__, __author__ # noqa +# When forming the command line involving __main__.py, it might be tempting to +# import it as a module, and then use its __file__. However, that does not work +# reliably, because __file__ can be a relative path - and when it is relative, +# that's relative to the current directory at the time import was done, which +# may be different from the current directory at the time the path is used. +# +# So, to be able to correctly locate the script at any point, we compute the +# absolute path at import time. +__file__ = os.path.abspath(__file__) + + ################################## # the script diff --git a/ptvsd/multiproc.py b/ptvsd/multiproc.py index 028e1ec9..34eab550 100644 --- a/ptvsd/multiproc.py +++ b/ptvsd/multiproc.py @@ -18,10 +18,10 @@ try: except ImportError: import Queue as queue -from . import options -from .socket import create_server, create_client -from .messaging import JsonIOStream, JsonMessageChannel -from ._util import new_hidden_thread, debug +from ptvsd import options +from ptvsd.socket import create_server, create_client +from ptvsd.messaging import JsonIOStream, JsonMessageChannel +from ptvsd._util import new_hidden_thread, debug from _pydev_bundle import pydev_monkey from _pydevd_bundle.pydevd_comm import get_global_debugger @@ -168,9 +168,7 @@ def patch_args(args): the result should be: - python -R -Q warn -m ptvsd --host localhost --port 0 ... -m app - - Note that the first -m above is interpreted by Python, and the second by ptvsd. + python -R -Q warn .../ptvsd/__main__.py --host localhost --port 0 ... -m app """ if not options.multiprocess: return args @@ -242,8 +240,9 @@ def patch_args(args): # Now we need to inject the ptvsd invocation right before the target. The target # itself can remain as is, because ptvsd is compatible with Python in that respect. + from ptvsd import __main__ args[i:i] = [ - '-m', 'ptvsd', + __main__.__file__, '--host', 'localhost', '--port', '0', '--wait', diff --git a/pytests/func/test_multiproc.py b/pytests/func/test_multiproc.py index d99ae9f0..c255a176 100644 --- a/pytests/func/test_multiproc.py +++ b/pytests/func/test_multiproc.py @@ -8,9 +8,9 @@ import platform import pytest import sys -from ..helpers.pattern import ANY -from ..helpers.session import DebugSession -from ..helpers.timeline import Event, Request +from pytests.helpers.pattern import ANY +from pytests.helpers.session import DebugSession +from pytests.helpers.timeline import Event, Request @pytest.mark.timeout(60) @@ -43,7 +43,7 @@ def test_multiprocessing(debug_session, pyfile): print('leaving child') if __name__ == '__main__': - import pytests.helpers.backchannel as backchannel + import backchannel if sys.version_info >= (3, 4): multiprocessing.set_start_method('spawn') else: @@ -144,7 +144,8 @@ def test_subprocess(debug_session, pyfile): @pyfile def child(): import sys - print(' '.join(sys.argv)) + import backchannel + backchannel.write_json(sys.argv) @pyfile def parent(): @@ -159,7 +160,7 @@ def test_subprocess(debug_session, pyfile): debug_session.multiprocess = True debug_session.program_args += [child] - debug_session.prepare_to_run(filename=parent) + debug_session.prepare_to_run(filename=parent, backchannel=True) debug_session.start_debugging() root_start_request, = debug_session.all_occurrences_of(Request('launch') | Request('attach')) @@ -180,15 +181,15 @@ def test_subprocess(debug_session, pyfile): } }) child_port = child_subprocess.body['port'] + debug_session.proceed() child_session = DebugSession(method='attach_socket', ptvsd_port=child_port) child_session.ignore_unobserved = debug_session.ignore_unobserved child_session.connect() child_session.handshake() child_session.start_debugging() - debug_session.proceed() - child_args_output = child_session.wait_for_next(Event('output')) - assert child_args_output.body['output'].endswith('child.py --arg1 --arg2 --arg3') + child_argv = debug_session.read_json() + assert child_argv == [child, '--arg1', '--arg2', '--arg3'] debug_session.wait_for_exit() diff --git a/pytests/func/test_run.py b/pytests/func/test_run.py index 8a30f158..f0773b37 100644 --- a/pytests/func/test_run.py +++ b/pytests/func/test_run.py @@ -20,7 +20,7 @@ def test_run(debug_session, pyfile, run_as): def code_to_debug(): import os import sys - from pytests.helpers import backchannel + import backchannel print('begin') assert backchannel.read_json() == 'continue' diff --git a/pytests/helpers/debuggee/__init__.py b/pytests/helpers/debuggee/__init__.py new file mode 100644 index 00000000..55957186 --- /dev/null +++ b/pytests/helpers/debuggee/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root +# for license information. + +from __future__ import print_function, with_statement, absolute_import + +# This dummy package contains modules that are only supposed to be imported from +# the code that is executed under debugger as part of the test (e.g. via @pyfile). +# PYTHONPATH has an entry appended to it that allows these modules to be imported +# directly from such code, i.e. "import backchannel". Consequently, these modules +# should not assume that any other code from pytests/ is importable. + + +# Ensure that __file__ is always absolute. +import os +__file__ = os.path.abspath(__file__) diff --git a/pytests/helpers/backchannel.py b/pytests/helpers/debuggee/backchannel.py similarity index 100% rename from pytests/helpers/backchannel.py rename to pytests/helpers/debuggee/backchannel.py diff --git a/pytests/helpers/session.py b/pytests/helpers/session.py index b2015872..7f331e5b 100644 --- a/pytests/helpers/session.py +++ b/pytests/helpers/session.py @@ -14,17 +14,15 @@ import time import traceback import ptvsd +import ptvsd.__main__ from ptvsd.messaging import JsonIOStream, JsonMessageChannel, MessageHandlers -from . import colors, print, watchdog + +from . import colors, debuggee, print, watchdog from .messaging import LoggingJsonStream from .pattern import ANY from .timeline import Timeline, Event, Response -# ptvsd.__file__ will be