Repository: microsoft/ptvsd

Issue:
Handle debugOptions sent by VS Code
Change to ensure we have a consistent approach to sending custom info to PTVSD from VS and VSC


Discussion:
@int19h @karthiknadig 
Had a chat with @ericsnowcurrently will not proceed with this change. Better to have specific properties.


Resolving patch:
diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py
--- a/ptvsd/wrapper.py
+++ b/ptvsd/wrapper.py
@@ -34,7 +34,7 @@
 import ptvsd.futures as futures  # noqa
 import ptvsd.untangle as untangle  # noqa
 from ptvsd.pathutils import PathUnNormcase  # noqa
-from ptvsd.safe_repr import SafeRepr # noqa
+from ptvsd.safe_repr import SafeRepr  # noqa
 
 
 __author__ = "Microsoft Corporation <ptvshelp@microsoft.com>"
@@ -617,7 +617,6 @@ def __init__(self, socket, pydevd, logfile=None,
         self.exceptions_mgr = ExceptionsManager(self)
         self.modules_mgr = ModulesManager(self)
         self.disconnect_request = None
-        self.launch_arguments = None
         self.debug_options = {}
         self.disconnect_request_event = threading.Event()
         pydevd._vscprocessor = self
@@ -792,34 +791,49 @@ def on_initialize(self, request, args):
     def on_configurationDone(self, request, args):
         # TODO: docstring
         self.send_response(request)
-        self.process_launch_arguments()
+        self.process_debug_options()
         self.pydevd_request(pydevd_comm.CMD_RUN, '')
 
-    def process_launch_arguments(self):
+    def process_debug_options(self):
         """
         Process the launch arguments to configure the debugger.
-        Further information can be found here https://code.visualstudio.com/docs/editor/debugging#_launchjson-attributes
-        {
-            type:'python',
-            request:'launch'|'attach',
-            name:'friendly name for debug config',
-            // Custom attributes supported by PTVSD.
-            redirectOutput:true|false,
-        }
         """  # noqa
-        if self.launch_arguments is None:
-            return
-
-        if self.launch_arguments.get('fixFilePathCase', False):
+        if self.debug_options.get('FIX_FILE_PATH_CASE', False):
             self.path_casing.enable()
 
-        if self.launch_arguments.get('redirectOutput', False) \
-            or self.debug_options.get('REDIRECT_OUTPUT', False):
+        if self.debug_options.get('REDIRECT_OUTPUT', False):
             redirect_output = 'STDOUT\tSTDERR'
         else:
             redirect_output = ''
         self.pydevd_request(pydevd_comm.CMD_REDIRECT_OUTPUT, redirect_output)
 
+    def build_debug_options(self, debug_options):
+        """
+        Build string representation of debug options from launch config (as provided by VSC)
+        Further information can be found here https://code.visualstudio.com/docs/editor/debugging#_launchjson-attributes
+        {
+            type:'python',
+            request:'launch'|'attach',
+            name:'friendly name for debug config',
+            debugOptions:[
+                'RedirectOutput', 'Django'
+            ]
+        }
+        """  # noqa
+        debug_option_mapping = {
+            'RedirectOutput': 'REDIRECT_OUTPUT=True',
+            'WaitOnNormalExit': 'WAIT_ON_NORMAL_EXIT=True',
+            'WaitOnAbnormalExit': 'WAIT_ON_ABNORMAL_EXIT=True',
+            'Django': 'DJANGO_DEBUG=True',
+            'Flask': 'FLASK_DEBUG=True',
+            'Jinja': 'FLASK_DEBUG=True',
+            'FixFilePathCase': 'FIX_FILE_PATH_CASE=True',
+            'DebugStdLib': 'DEBUG_STD_LIB=True'
+        }
+        return ';'.join(debug_option_mapping[option]
+                        for option in debug_options
+                        if option in debug_option_mapping)
+
     def _parse_debug_options(self, debug_options):
         """Debug options are semicolon separated key=value pairs
             WAIT_ON_ABNORMAL_EXIT=True|False
@@ -872,9 +886,9 @@ def on_attach(self, request, args):
     def on_launch(self, request, args):
         # TODO: docstring
         self.start_reason = 'launch'
-        self.launch_arguments = request.get('arguments', None)
+        options = self.build_debug_options(args.get('debugOptions', []))
         self.debug_options = self._parse_debug_options(
-            args.get('options', ''))
+            args.get('options', options))
         self.send_response(request)
 
     def send_process_event(self, start_method):
@@ -1196,11 +1210,11 @@ def on_evaluate(self, request, args):
             try:
                 xml2 = untangle.parse(resp_args).xml
                 xvar2 = xml2.var
-                result_type  = unquote(xvar2['type'])
+                result_type = unquote(xvar2['type'])
                 result = unquote(xvar2['value'])
             except Exception:
                 # if resp_args is not xml then it contains the error traceback
-                result_type  = unquote(xvar['type'])
+                result_type = unquote(xvar['type'])
                 result = unquote(xvar['value'])
             self.send_response(
                 request,


Test patch:
diff --git a/tests/highlevel/test_lifecycle.py b/tests/highlevel/test_lifecycle.py
--- a/tests/highlevel/test_lifecycle.py
+++ b/tests/highlevel/test_lifecycle.py
@@ -88,6 +88,7 @@ def test_attach(self):
         self.assert_received(self.debugger, [
             self.debugger_msgs.new_request(CMD_VERSION,
                                            *['1.1', OS_ID, 'ID']),
+            self.debugger_msgs.new_request(CMD_REDIRECT_OUTPUT),
             self.debugger_msgs.new_request(CMD_RUN),
         ])
 
diff --git a/tests/highlevel/test_messages.py b/tests/highlevel/test_messages.py
--- a/tests/highlevel/test_messages.py
+++ b/tests/highlevel/test_messages.py
@@ -1131,6 +1131,80 @@ def test_vs_flask_jinja2(self):
                 '2\tjinja2-line\teggs.html\t17\tNone\tNone\tNone'),
         ])
 
+    def test_vsc_flask_jinja2(self):
+        with self.launched(args={'debugOptions': ['Flask']}):
+            self.send_request(
+                source={'path': 'spam.py'},
+                breakpoints=[{'line': '10'}],
+            )
+            self.send_request(
+                source={'path': 'eggs.html'},
+                breakpoints=[{'line': '17'}],
+            )
+            received = self.vsc.received
+
+        self.assert_vsc_received(received, [
+            self.expected_response(
+                breakpoints=[
+                    {'id': 1,
+                     'verified': True,
+                     'line': '10'},
+                ],
+            ),
+            self.expected_response(
+                breakpoints=[
+                    {'id': 2,
+                     'verified': True,
+                     'line': '17'},
+                ],
+            ),
+        ])
+
+        self.PYDEVD_CMD = CMD_SET_BREAK
+        self.assert_received(self.debugger, [
+            self.expected_pydevd_request(
+                '1\tpython-line\tspam.py\t10\tNone\tNone\tNone'),
+            self.expected_pydevd_request(
+                '2\tjinja2-line\teggs.html\t17\tNone\tNone\tNone'),
+        ])
+
+    def test_vsc_jinja2(self):
+        with self.launched(args={'debugOptions': ['Jinja']}):
+            self.send_request(
+                source={'path': 'spam.py'},
+                breakpoints=[{'line': '10'}],
+            )
+            self.send_request(
+                source={'path': 'eggs.html'},
+                breakpoints=[{'line': '17'}],
+            )
+            received = self.vsc.received
+
+        self.assert_vsc_received(received, [
+            self.expected_response(
+                breakpoints=[
+                    {'id': 1,
+                     'verified': True,
+                     'line': '10'},
+                ],
+            ),
+            self.expected_response(
+                breakpoints=[
+                    {'id': 2,
+                     'verified': True,
+                     'line': '17'},
+                ],
+            ),
+        ])
+
+        self.PYDEVD_CMD = CMD_SET_BREAK
+        self.assert_received(self.debugger, [
+            self.expected_pydevd_request(
+                '1\tpython-line\tspam.py\t10\tNone\tNone\tNone'),
+            self.expected_pydevd_request(
+                '2\tjinja2-line\teggs.html\t17\tNone\tNone\tNone'),
+        ])
+
 
 class SetFunctionBreakpointsTests(NormalRequestTest, unittest.TestCase):