From d27b70da19ea9ff2855e2f61f20dbdbb53757769 Mon Sep 17 00:00:00 2001 From: OlegWock Date: Thu, 27 Feb 2025 12:04:26 +0100 Subject: [PATCH 1/4] Port custom changes from old fork of python language server --- .gitignore | 2 ++ pylsp/__main__.py | 8 +++-- pylsp/hookspecs.py | 5 ++++ pylsp/plugins/jedi_completion.py | 19 ++++++++++++ pylsp/plugins/preload_imports.py | 51 ++++---------------------------- pylsp/python_lsp.py | 44 ++++++++++++++++++++------- pylsp/workspace.py | 30 +++++++++++++++---- 7 files changed, 95 insertions(+), 64 deletions(-) diff --git a/.gitignore b/.gitignore index 3c4093d1..d04280b6 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,5 @@ ENV/ # Special files .DS_Store *.temp + +.venv \ No newline at end of file diff --git a/pylsp/__main__.py b/pylsp/__main__.py index 44aa3cfa..372615ce 100644 --- a/pylsp/__main__.py +++ b/pylsp/__main__.py @@ -74,9 +74,11 @@ def main() -> None: _configure_logger(args.verbose, args.log_config, args.log_file) if args.tcp: - start_tcp_lang_server( - args.host, args.port, args.check_parent_process, PythonLSPServer - ) + while True: + start_tcp_lang_server( + args.host, args.port, args.check_parent_process, PythonLSPServer + ) + time.sleep(0.500) elif args.ws: start_ws_lang_server(args.port, args.check_parent_process, PythonLSPServer) else: diff --git a/pylsp/hookspecs.py b/pylsp/hookspecs.py index 41508be1..a47ed984 100644 --- a/pylsp/hookspecs.py +++ b/pylsp/hookspecs.py @@ -28,6 +28,11 @@ def pylsp_completions(config, workspace, document, position, ignored_names) -> N pass +@hookspec +def pyls_completion_detail(config, item) -> None: + pass + + @hookspec(firstresult=True) def pylsp_completion_item_resolve(config, workspace, document, completion_item) -> None: pass diff --git a/pylsp/plugins/jedi_completion.py b/pylsp/plugins/jedi_completion.py index 2796a093..7a115a96 100644 --- a/pylsp/plugins/jedi_completion.py +++ b/pylsp/plugins/jedi_completion.py @@ -28,6 +28,8 @@ "statement": lsp.CompletionItemKind.Variable, } +COMPLETION_CACHE = {} + # Types of parso nodes for which snippet is not included in the completion _IMPORTS = ("import_name", "import_from") @@ -135,6 +137,22 @@ def pylsp_completions(config, document, position): return ready_completions or None +@hookimpl +def pyls_completion_detail(config, item): + d = COMPLETION_CACHE.get(item) + if d: + completion = { + 'label': '', #_label(d), + 'kind': _TYPE_MAP.get(d.type), + 'detail': '', #_detail(d), + 'documentation': _utils.format_docstring(d.docstring()), + 'sortText': '', #_sort_text(d), + 'insertText': d.name + } + return completion + else: + log.info('Completion missing') + return None @hookimpl def pylsp_completion_item_resolve(config, completion_item, document): @@ -229,6 +247,7 @@ def _format_completion( resolve_label_or_snippet=False, snippet_support=False, ): + COMPLETION_CACHE[d.name] = d completion = { "label": _label(d, resolve_label_or_snippet), "kind": _TYPE_MAP.get(d.type), diff --git a/pylsp/plugins/preload_imports.py b/pylsp/plugins/preload_imports.py index ebcd9adb..a8965b7a 100644 --- a/pylsp/plugins/preload_imports.py +++ b/pylsp/plugins/preload_imports.py @@ -8,52 +8,11 @@ log = logging.getLogger(__name__) MODULES = [ - "OpenGL", - "PIL", - "array", - "audioop", - "binascii", - "cPickle", - "cStringIO", - "cmath", - "collections", - "datetime", - "errno", - "exceptions", - "gc", - "imageop", - "imp", - "itertools", - "marshal", - "math", - "matplotlib", - "mmap", - "mpmath", - "msvcrt", - "networkx", - "nose", - "nt", - "numpy", - "operator", - "os", - "os.path", - "pandas", - "parser", - "rgbimg", - "scipy", - "signal", - "skimage", - "sklearn", - "statsmodels", - "strop", - "sympy", - "sys", - "thread", - "time", - "wx", - "xxsubtype", - "zipimport", - "zlib", + "numpy", "tensorflow", "sklearn", "array", "binascii", "cmath", "collections", + "datetime", "errno", "exceptions", "gc", "imageop", "imp", "itertools", + "marshal", "math", "matplotlib", "mmap", "mpmath", "msvcrt", "networkx", "nose", "nt", + "operator", "os", "os.path", "pandas", "parser", "scipy", "signal", + "skimage", "statsmodels", "strop", "sympy", "sys", "thread", "time", "wx", "zlib" ] diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index ba41d6aa..2d1ece56 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -8,6 +8,7 @@ import uuid from functools import partial from typing import Any, Dict, List +from hashlib import sha256 try: import ujson as json @@ -43,17 +44,36 @@ def setup(self) -> None: self.delegate = self.DELEGATE_CLASS(self.rfile, self.wfile) def handle(self) -> None: - try: - self.delegate.start() - except OSError as e: - if os.name == "nt": - # Catch and pass on ConnectionResetError when parent process - # dies - if isinstance(e, WindowsError) and e.winerror == 10054: - pass + self.auth(self.delegate.start) self.SHUTDOWN_CALL() + def auth(self, cb): + token = '' + if "JUPYTER_TOKEN" in os.environ: + token = os.environ["JUPYTER_TOKEN"] + else: + log.warn('! Missing jupyter token !') + + data = self.rfile.readline() + try: + auth_req = json.loads(data.decode().split('\n')[0]) + except: + log.error('Error parsing authentication message') + auth_error_msg = { 'msg': 'AUTH_ERROR' } + self.wfile.write(json.dumps(auth_error_msg).encode()) + return + + hashed_token = sha256(token.encode()).hexdigest() + if auth_req.get('token') == hashed_token: + auth_success_msg = { 'msg': 'AUTH_SUCCESS' } + self.wfile.write(json.dumps(auth_success_msg).encode()) + cb() + else: + log.info('Failed to authenticate: invalid credentials') + auth_invalid_msg = { 'msg': 'AUTH_INVALID_CRED' } + self.wfile.write(json.dumps(auth_invalid_msg).encode()) + def start_tcp_lang_server(bind_addr, port, check_parent_process, handler_class) -> None: if not issubclass(handler_class, PythonLSPServer): @@ -401,7 +421,11 @@ def completions(self, doc_uri, position): completions = self._hook( "pylsp_completions", doc_uri, position=position, ignored_names=ignored_names ) - return {"isIncomplete": False, "items": flatten(completions)} + return {"isIncomplete": True, "items": flatten(completions)} + + def completion_detail(self, item): + detail = self._hook('pyls_completion_detail', item=item) + return detail def completion_item_resolve(self, completion_item): doc_uri = completion_item.get("data", {}).get("doc_uri", None) @@ -543,7 +567,7 @@ def folding(self, doc_uri): return flatten(self._hook("pylsp_folding_range", doc_uri)) def m_completion_item__resolve(self, **completionItem): - return self.completion_item_resolve(completionItem) + return self.completion_detail(completionItem.get('label')) def m_notebook_document__did_open( self, notebookDocument=None, cellTextDocuments=None, **_kwargs diff --git a/pylsp/workspace.py b/pylsp/workspace.py index 846527f6..ad10b47f 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -10,6 +10,7 @@ from contextlib import contextmanager from threading import RLock from typing import Callable, Generator, List, Optional +import importlib.metadata import jedi @@ -392,6 +393,18 @@ def close(self) -> None: class Document: + DO_NOT_PRELOAD_MODULES = ['attrs', 'backcall', 'bleach', 'certifi', 'chardet', 'cycler', 'decorator', 'defusedxml', + 'docopt', 'entrypoints', 'idna', 'importlib-metadata', 'ipykernel', 'ipython-genutils', + 'ipython', 'ipywidgets', 'jedi', 'jinja2', 'joblib', 'jsonschema', 'jupyter-client', + 'jupyter-core', 'markupsafe', 'mistune', 'nbconvert', 'nbformat', 'notebook', 'packaging', + 'pandocfilters', 'parso', 'pexpect', 'pickleshare', 'pip', 'pipreqs', 'pluggy', + 'prometheus-client', 'prompt-toolkit', 'ptyprocess', 'pygments', 'pyparsing', + 'pyrsistent', 'python-dateutil', 'python-jsonrpc-server', 'python-language-server', + 'pytz', 'pyzmq', 'send2trash', 'setuptools', 'six', 'terminado', 'testpath', + 'threadpoolctl', 'tornado', 'traitlets', 'ujson', 'wcwidth', 'webencodings', 'wheel', + 'widgetsnbextension', 'yarg', 'zipp'] + + def __init__( self, uri, @@ -417,6 +430,15 @@ def __init__( self._rope_project_builder = rope_project_builder self._lock = RLock() + jedi.settings.cache_directory = '.cache/jedi/' + jedi.settings.use_filesystem_cache = True + jedi.settings.auto_import_modules = self._get_auto_import_modules() + + def _get_auto_import_modules(self): + installed_packages_list = [dist.metadata['Name'] for dist in importlib.metadata.distributions()] + auto_import_modules = [pkg for pkg in installed_packages_list if pkg not in self.DO_NOT_PRELOAD_MODULES] + return auto_import_modules + def __str__(self): return str(self.uri) @@ -546,12 +568,11 @@ def jedi_script(self, position=None, use_document_path=False): env_vars = os.environ.copy() env_vars.pop("PYTHONPATH", None) - environment = self.get_enviroment(environment_path, env_vars=env_vars) sys_path = self.sys_path( environment_path, env_vars, prioritize_extra_paths, extra_paths ) - project_path = self._workspace.root_path + import __main__ # Extend sys_path with document's path if requested if use_document_path: @@ -560,15 +581,14 @@ def jedi_script(self, position=None, use_document_path=False): kwargs = { "code": self.source, "path": self.path, - "environment": environment if environment_path else None, - "project": jedi.Project(path=project_path, sys_path=sys_path), + 'namespaces': [__main__.__dict__] } if position: # Deprecated by Jedi to use in Script() constructor kwargs += _utils.position_to_jedi_linecolumn(self, position) - return jedi.Script(**kwargs) + return jedi.Interpreter(**kwargs) def get_enviroment(self, environment_path=None, env_vars=None): # TODO(gatesn): #339 - make better use of jedi environments, they seem pretty powerful From 3fe666112c8f895e9c6456b1c1e2530944237353 Mon Sep 17 00:00:00 2001 From: OlegWock Date: Thu, 27 Feb 2025 14:17:43 +0100 Subject: [PATCH 2/4] Adjust format of returned hover info to match previous language server --- pylsp/plugins/hover.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pylsp/plugins/hover.py b/pylsp/plugins/hover.py index ca69d1b3..f10557b7 100644 --- a/pylsp/plugins/hover.py +++ b/pylsp/plugins/hover.py @@ -30,6 +30,8 @@ def pylsp_hover(config, document, position): supported_markup_kinds = hover_capabilities.get("contentFormat", ["markdown"]) preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) + doc = _utils.format_docstring(definition.docstring(raw=True), preferred_markup_kind)["value"] + # Find first exact matching signature signature = next( ( @@ -40,11 +42,16 @@ def pylsp_hover(config, document, position): "", ) + contents = [] + if signature: + contents.append({ + 'language': 'python', + 'value': signature, + }) + + if doc: + contents.append(doc) + return { - "contents": _utils.format_docstring( - # raw docstring returns only doc, without signature - definition.docstring(raw=True), - preferred_markup_kind, - signatures=[signature] if signature else None, - ) + "contents": contents or '' } From d89eacb2c79b32f9674661c2f7b5c4e1a71ed664 Mon Sep 17 00:00:00 2001 From: OlegWock Date: Thu, 27 Feb 2025 14:33:26 +0100 Subject: [PATCH 3/4] Fix typo --- pylsp/hookspecs.py | 2 +- pylsp/plugins/jedi_completion.py | 2 +- pylsp/python_lsp.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pylsp/hookspecs.py b/pylsp/hookspecs.py index a47ed984..d9390f28 100644 --- a/pylsp/hookspecs.py +++ b/pylsp/hookspecs.py @@ -29,7 +29,7 @@ def pylsp_completions(config, workspace, document, position, ignored_names) -> N @hookspec -def pyls_completion_detail(config, item) -> None: +def pylsp_completion_detail(config, item) -> None: pass diff --git a/pylsp/plugins/jedi_completion.py b/pylsp/plugins/jedi_completion.py index 7a115a96..e5e7d7b0 100644 --- a/pylsp/plugins/jedi_completion.py +++ b/pylsp/plugins/jedi_completion.py @@ -138,7 +138,7 @@ def pylsp_completions(config, document, position): return ready_completions or None @hookimpl -def pyls_completion_detail(config, item): +def pylsp_completion_detail(config, item): d = COMPLETION_CACHE.get(item) if d: completion = { diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index 2d1ece56..53c6b06b 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -424,7 +424,7 @@ def completions(self, doc_uri, position): return {"isIncomplete": True, "items": flatten(completions)} def completion_detail(self, item): - detail = self._hook('pyls_completion_detail', item=item) + detail = self._hook('pylsp_completion_detail', item=item) return detail def completion_item_resolve(self, completion_item): From 44e322cac95a000bb18a4acc19b027ba96c2d0e9 Mon Sep 17 00:00:00 2001 From: OlegWock Date: Thu, 27 Feb 2025 15:08:24 +0100 Subject: [PATCH 4/4] Follow old format for signature help --- pylsp/plugins/signature.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylsp/plugins/signature.py b/pylsp/plugins/signature.py index 7ad5b208..c07fc8dc 100644 --- a/pylsp/plugins/signature.py +++ b/pylsp/plugins/signature.py @@ -45,7 +45,7 @@ def pylsp_signature_help(config, document, position): "label": function_sig, "documentation": _utils.format_docstring( s.docstring(raw=True), markup_kind=preferred_markup_kind - ), + )["value"], } # If there are params, add those @@ -55,7 +55,7 @@ def pylsp_signature_help(config, document, position): "label": p.name, "documentation": _utils.format_docstring( _param_docs(docstring, p.name), markup_kind=preferred_markup_kind - ), + )["value"], } for p in s.params ]