Repository: microsoft/ptvsd

Issue:
Launching module with --nodebug breaks imports
My project structure is 
```
demo/
  foo/
    __main__.py
    bar.py
```
`bar.py` is an empty file. `__main__.py` contains the single statement `import bar`. My `launch.json` configuration is
```
{
    "name": "Demo",
    "type": "pythonExperimental",
    "request": "launch",
    "program": "${workspaceFolder}/foo",
    "console": "integratedTerminal"
}
```

Running in debug mode causes the program to immediately terminate successfully, as expected. With `--nodebug` the program crashes with a `ModuleNotFoundError` exception complaining that there is no module named bar.

I am running `ptvsd` in VSCode with Python3.6 on Ubuntu 18.04.


Discussion:
Can you try this configuration?
```json
        {
            "name": "Module: foo",
            "type": "pythonExperimental",
            "request": "launch",
            "console": "integratedTerminal",
            "module": "foo",
            "cwd": "${workspaceFolder}",
            "env": {"PYTHONPATH": "${workspaceFolder}"}
        },
```
Fails with the same error.
Can you try it with an empty `__init__.py` file? You currently have `__main__.py`, and `bar.py`. Add a `__init__.py`.
I did, same issue.
Can you tell me the version of `ptvsd`? I am not seeing this in master. But I want to make sure we have addressed this.
I'm using 4.1.1a10, which ships with the current version of vscode.

Actually I'd like to clarify that the configuration you gave above (with `module` set instead of `program`) actually fails with and without `--nodebug`. Presumably this happens since it's trying to run something like `python -m ptvsd -m foo` which _should_ fail since bar needs to be a relative import. But with the original configurations vscode runs something like `python -m ptvsd /path/to/foo` which works fine, but `python -m ptvsd --nodebug /path/to/foo` fails with the error mention in the OP.

To add some motivation for this bug, I found it when working on a Django app, which internally uses some wonky imports that break when running ptvsd in nodebug mode.
With the configuration i gave it should work if you have `__init__.py`. However, there seems to be a issue with namespace packages, in that case too. I will look into this.
This is not a bug -- things are working as expected.

i.e.: if you have a structure such as the one below where `demo` is the folder in the `PYTHONPATH`, you should write an import on `__main__.py` as `from foo import bar` or `from . import bar` (a single `import bar` expects the `bar` module to be on the root -- which is not the case).

```
demo/
  foo/
    __main__.py
    bar.py
```

As a note, I suggest you don't use relative imports on this case, only full imports (because if it's a relative import it won't work unless python is executed with the `-m` flag -- when executed passing the `__main__.py` file itself it'd fail).
Regardless of how you feel imports should be done, the fact remains that `python foo` will execute the code without throwing an exception, as will `python -m ptvsd foo`. Passing the `--nodebug` option should break this behaviour.

Incidentally, I agree that `import bar` is a poor way to perform the import. However, projects like Django will perform imports this way e.g. when setting `DJANGO_SETTINGS_MODULE`, so this bug will break programs that ought to work.
Note that it'll fail in the command line if `python -m foo` is used in plain Python, so, ideally you wouldn't write imports that way, but I agree with your point that `ptvsd` should replicate what Python does even if it means shooting yourself in the foot ;) 

-- Python itself appears to be executing `foo/__main__.py` directly (so, it'll find `bar` as a root module -- and if you do in another place an `from foo import bar` you'll have another version of the same module).

Resolving patch:
diff --git a/ptvsd/_vendored/pydevd/pydevd.py b/ptvsd/_vendored/pydevd/pydevd.py
--- a/ptvsd/_vendored/pydevd/pydevd.py
+++ b/ptvsd/_vendored/pydevd/pydevd.py
@@ -1018,6 +1018,7 @@ def run(self, file, globals=None, locals=None, is_module=False, set_trace=True):
             if os.path.isfile(new_target):
                 file = new_target
 
+        m = None
         if globals is None:
             m = save_main_module(file, 'pydevd')
             globals = m.__dict__
@@ -1029,27 +1030,28 @@ def run(self, file, globals=None, locals=None, is_module=False, set_trace=True):
         if locals is None:
             locals = globals
 
+        # Predefined (writable) attributes: __name__ is the module's name;
+        # __doc__ is the module's documentation string, or None if unavailable;
+        # __file__ is the pathname of the file from which the module was loaded,
+        # if it was loaded from a file. The __file__ attribute is not present for
+        # C modules that are statically linked into the interpreter; for extension modules
+        # loaded dynamically from a shared library, it is the pathname of the shared library file.
+
+
+        # I think this is an ugly hack, bug it works (seems to) for the bug that says that sys.path should be the same in
+        # debug and run.
+        if sys.path[0] != '' and m is not None and m.__file__.startswith(sys.path[0]):
+            # print >> sys.stderr, 'Deleting: ', sys.path[0]
+            del sys.path[0]
+
+        if not is_module:
+            # now, the local directory has to be added to the pythonpath
+            # sys.path.insert(0, os.getcwd())
+            # Changed: it's not the local directory, but the directory of the file launched
+            # The file being run must be in the pythonpath (even if it was not before)
+            sys.path.insert(0, os.path.split(rPath(file))[0])
+
         if set_trace:
-            # Predefined (writable) attributes: __name__ is the module's name;
-            # __doc__ is the module's documentation string, or None if unavailable;
-            # __file__ is the pathname of the file from which the module was loaded,
-            # if it was loaded from a file. The __file__ attribute is not present for
-            # C modules that are statically linked into the interpreter; for extension modules
-            # loaded dynamically from a shared library, it is the pathname of the shared library file.
-
-
-            # I think this is an ugly hack, bug it works (seems to) for the bug that says that sys.path should be the same in
-            # debug and run.
-            if sys.path[0] != '' and m.__file__.startswith(sys.path[0]):
-                # print >> sys.stderr, 'Deleting: ', sys.path[0]
-                del sys.path[0]
-
-            if not is_module:
-                # now, the local directory has to be added to the pythonpath
-                # sys.path.insert(0, os.getcwd())
-                # Changed: it's not the local directory, but the directory of the file launched
-                # The file being run must be in the pythonpath (even if it was not before)
-                sys.path.insert(0, os.path.split(rPath(file))[0])
 
             while not self.ready_to_run:
                 time.sleep(0.1)  # busy wait until we receive run command


Test patch:
diff --git a/ptvsd/_vendored/pydevd/tests_python/resources/launch/foo/__main__.py b/ptvsd/_vendored/pydevd/tests_python/resources/launch/foo/__main__.py
new file mode 100644
--- /dev/null
+++ b/ptvsd/_vendored/pydevd/tests_python/resources/launch/foo/__main__.py
@@ -0,0 +1,2 @@
+import bar
+print('Worked')
\ No newline at end of file
diff --git a/ptvsd/_vendored/pydevd/tests_python/resources/launch/foo/bar.py b/ptvsd/_vendored/pydevd/tests_python/resources/launch/foo/bar.py
new file mode 100644
diff --git a/ptvsd/_vendored/pydevd/tests_python/test_run.py b/ptvsd/_vendored/pydevd/tests_python/test_run.py
new file mode 100644
--- /dev/null
+++ b/ptvsd/_vendored/pydevd/tests_python/test_run.py
@@ -0,0 +1,35 @@
+pytest_plugins = [
+    str('_pytest.pytester'),
+]
+
+def _run_and_check(testdir, path):
+    result = testdir.runpython(path)
+    result.stdout.fnmatch_lines([
+        'Worked'
+    ])
+
+def test_run(testdir):
+    from tests_python import debugger_unittest
+    from os.path import os
+    
+    foo_dir = debugger_unittest._get_debugger_test_file(os.path.join('resources', 'launch', 'foo'))
+    pydevd_dir = os.path.dirname(os.path.dirname(__file__))
+    assert os.path.exists(os.path.join(pydevd_dir, 'pydevd.py'))
+    
+    _run_and_check(testdir, testdir.makepyfile('''
+import sys
+sys.path.append(%(pydevd_dir)r)
+import pydevd
+py_db = pydevd.PyDB()
+py_db.ready_to_run = True
+py_db.run(%(foo_dir)r)
+''' % locals()))
+    
+    _run_and_check(testdir, testdir.makepyfile('''
+import sys
+sys.path.append(%(pydevd_dir)r)
+import pydevd
+py_db = pydevd.PyDB()
+py_db.run(%(foo_dir)r, set_trace=False)
+''' % locals()))
+