Repository: python-lsp/python-lsp-server
Pull request #154 (2022-01-12 23:23:34)

Prevent faulty third-party plugins to crash the server
I'm using this in spyder. There on loading pylsp-black, I get this error:

```
  File "C:\Program Files\Python39\lib\site-packages\pylsp_jsonrpc\endpoint.py", line 116, in consume
    self._handle_request(message['id'], message['method'], message.get('params'))
  File "C:\Program Files\Python39\lib\site-packages\pylsp_jsonrpc\endpoint.py", line 185, in _handle_request
    handler_result = handler(params)
  File "C:\Program Files\Python39\lib\site-packages\pylsp_jsonrpc\dispatchers.py", line 25, in handler
    return method(**(params or {}))
  File "C:\Program Files\Python39\lib\site-packages\pylsp\python_lsp.py", line 210, in m_initialize
    self.config = config.Config(rootUri, initializationOptions or {},
  File "C:\Program Files\Python39\lib\site-packages\pylsp\config\config.py", line 52, in __init__
    entry_point.load()
  File "C:\Program Files\Python39\lib\site-packages\pkg_resources\__init__.py", line 2449, in load
    self.require(*args, **kwargs)
  File "C:\Program Files\Python39\lib\site-packages\pkg_resources\__init__.py", line 2472, in require
    items = working_set.resolve(reqs, env, installer, extras=self.extras)
  File "C:\Program Files\Python39\lib\site-packages\pkg_resources\__init__.py", line 772, in resolve
    raise DistributionNotFound(req, requirers)

pkg_resources.DistributionNotFound: The 'toml' distribution was not found and is required by the application
```

Which prevents LSP to start (in the status bar, LSP starting spins indefenitely) 
not sure why it's not the expected ImportError, but when also catching the pkg_resources.DistributionNotFound spyder seems to work correctly minus the LSP plugin.
Not sure whether this is the way to go, but it seems to me in the spirit of the context, that is catching plugin's errors on loading.

ccordoba12: Hey @arian-f, thanks a lot for your contribution! I think this is an error in one of the PyLSP plugins, so it shouldn't be solved this way, but in the plugin itself.

Could you post the output of `pip list` to check what plugins you have installed? 
arian-f: yes, it's caused by `pylsp-black`

`2022-01-13 00:02:48,874 W. Europe Standard Time - WARNING - pylsp.config.config - Failed to load pylsp entry point 'pylsp_black': The 'toml' distribution was not found and is required by the application
`
arian-f: if you decide not to handle this kind of exception, it may still be beneficial to catch any exceptions there, make a warning of the offending plugin and then reraise the excpetion. As you see in the trace above, the offending plugin is not in the trace but rather it goes to pkg_resources. pkg_resources indicates a problem with toml, but the problem may as well be with pylsp-black not correctly specifying its dependencies. (Hope it's clear what's meant)

With the exception caught the offending plugin is printed.

Patch:
diff --git a/pylsp/_utils.py b/pylsp/_utils.py
index 9ac30cf..0732067 100644
--- a/pylsp/_utils.py
+++ b/pylsp/_utils.py
@@ -6,12 +6,17 @@ import inspect
 import logging
 import os
 import pathlib
+import re
 import threading
 
 import jedi
 
 JEDI_VERSION = jedi.__version__
 
+# Eol chars accepted by the LSP protocol
+EOL_CHARS = ['\r\n', '\r', '\n']
+EOL_REGEX = re.compile(f'({"|".join(EOL_CHARS)})')
+
 log = logging.getLogger(__name__)
 
 
@@ -220,3 +225,11 @@ else:
             return e.errno == errno.EPERM
         else:
             return True
+
+
+def get_eol_chars(text):
+    """Get EOL chars used in text."""
+    match = EOL_REGEX.search(text)
+    if match:
+        return match.group(0)
+    return None
diff --git a/pylsp/config/config.py b/pylsp/config/config.py
index 053dd23..498c645 100644
--- a/pylsp/config/config.py
+++ b/pylsp/config/config.py
@@ -50,7 +50,7 @@ class Config:
         for entry_point in pkg_resources.iter_entry_points(PYLSP):
             try:
                 entry_point.load()
-            except ImportError as e:
+            except Exception as e:  # pylint: disable=broad-except
                 log.warning("Failed to load %s entry point '%s': %s", PYLSP, entry_point.name, e)
                 self._pm.set_blocked(entry_point.name)
 
diff --git a/pylsp/plugins/autopep8_format.py b/pylsp/plugins/autopep8_format.py
index f62443f..8915fb7 100644
--- a/pylsp/plugins/autopep8_format.py
+++ b/pylsp/plugins/autopep8_format.py
@@ -2,9 +2,12 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import logging
+
 import pycodestyle
 from autopep8 import fix_code, continued_indentation as autopep8_c_i
+
 from pylsp import hookimpl
+from pylsp._utils import get_eol_chars
 
 log = logging.getLogger(__name__)
 
@@ -38,15 +41,27 @@ def _format(config, document, line_range=None):
     del pycodestyle._checks['logical_line'][pycodestyle.continued_indentation]
     pycodestyle.register_check(autopep8_c_i)
 
-    new_source = fix_code(document.source, options=options)
+    # Autopep8 doesn't work with CR line endings, so we replace them by '\n'
+    # and restore them below.
+    replace_cr = False
+    source = document.source
+    eol_chars = get_eol_chars(source)
+    if eol_chars == '\r':
+        replace_cr = True
+        source = source.replace('\r', '\n')
+
+    new_source = fix_code(source, options=options)
 
     # Switch it back
     del pycodestyle._checks['logical_line'][autopep8_c_i]
     pycodestyle.register_check(pycodestyle.continued_indentation)
 
-    if new_source == document.source:
+    if new_source == source:
         return []
 
+    if replace_cr:
+        new_source = new_source.replace('\n', '\r')
+
     # I'm too lazy at the moment to parse diffs into TextEdit items
     # So let's just return the entire file...
     return [{
diff --git a/pylsp/plugins/yapf_format.py b/pylsp/plugins/yapf_format.py
index 6728007..1c90f96 100644
--- a/pylsp/plugins/yapf_format.py
+++ b/pylsp/plugins/yapf_format.py
@@ -3,9 +3,12 @@
 
 import logging
 import os
+
 from yapf.yapflib import file_resources
 from yapf.yapflib.yapf_api import FormatCode
+
 from pylsp import hookimpl
+from pylsp._utils import get_eol_chars
 
 log = logging.getLogger(__name__)
 
@@ -34,8 +37,17 @@ def pylsp_format_range(document, range):  # pylint: disable=redefined-builtin
 
 
 def _format(document, lines=None):
+    # Yapf doesn't work with CR line endings, so we replace them by '\n'
+    # and restore them below.
+    replace_cr = False
+    source = document.source
+    eol_chars = get_eol_chars(source)
+    if eol_chars == '\r':
+        replace_cr = True
+        source = source.replace('\r', '\n')
+
     new_source, changed = FormatCode(
-        document.source,
+        source,
         lines=lines,
         filename=document.filename,
         style_config=file_resources.GetDefaultStyleForDir(
@@ -46,6 +58,9 @@ def _format(document, lines=None):
     if not changed:
         return []
 
+    if replace_cr:
+        new_source = new_source.replace('\n', '\r')
+
     # I'm too lazy at the moment to parse diffs into TextEdit items
     # So let's just return the entire file...
     return [{


Test patch:
diff --git a/test/plugins/test_autopep8_format.py b/test/plugins/test_autopep8_format.py
index cfb7bb6..1e790aa 100644
--- a/test/plugins/test_autopep8_format.py
+++ b/test/plugins/test_autopep8_format.py
@@ -71,3 +71,10 @@ def test_hanging_indentation(config, workspace):
 
     assert len(res) == 1
     assert res[0]['newText'] == CORRECT_INDENTED_DOC
+
+
+def test_cr_line_endings(config, workspace):
+    doc = Document(DOC_URI, workspace, 'import os;import sys\r\rdict(a=1)')
+    res = pylsp_format_document(config, doc)
+
+    assert res[0]['newText'] == 'import os\rimport sys\r\rdict(a=1)\r'
diff --git a/test/plugins/test_yapf_format.py b/test/plugins/test_yapf_format.py
index 9041043..4346985 100644
--- a/test/plugins/test_yapf_format.py
+++ b/test/plugins/test_yapf_format.py
@@ -58,3 +58,10 @@ def test_config_file(tmpdir, workspace):
 
     # A was split on multiple lines because of column_limit from config file
     assert pylsp_format_document(doc)[0]['newText'] == "A = [\n    'h', 'w',\n    'a'\n]\n\nB = ['h', 'w']\n"
+
+
+def test_cr_line_endings(workspace):
+    doc = Document(DOC_URI, workspace, 'import os;import sys\r\rdict(a=1)')
+    res = pylsp_format_document(doc)
+
+    assert res[0]['newText'] == 'import os\rimport sys\r\rdict(a=1)\r'