-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add PipClientHandler implementation (#41)
* Add ServerPipResource server interface implementation This is a server resource implementation that manages a pip-based server from dependencies provided through the "requirements.txt" file. Also: - remote "GenericClientHandler.get_storage_path()" API and just use "GenericClientHanlder.storage_path()" instead which is provided by both AbstractPlugin and LanguageHandler - Document storage_path() and package_storage() of GenericClientHandler
- Loading branch information
Showing
11 changed files
with
204 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
from .generic_client_handler import GenericClientHandler | ||
from .server_pip_resource import ServerPipResource | ||
from .server_resource_interface import ServerResourceInterface | ||
from LSP.plugin.core.typing import Any, Optional | ||
|
||
__all__ = ['PipClientHandler'] | ||
|
||
|
||
class PipClientHandler(GenericClientHandler): | ||
""" | ||
An implementation of :class:`GenericClientHandler` for handling pip-based LSP plugins. | ||
Automatically manages a pip-based server by installing and updating dependencies based on provided | ||
`requirements.txt` file. | ||
""" | ||
__server = None # type: Optional[ServerPipResource] | ||
|
||
requirements_txt_path = '' | ||
""" | ||
The path to the `requirements.txt` file containing a list of dependencies required by the server. | ||
If the package `LSP-foo` has a `requirements.txt` file at the root then the path will be just `requirements.txt`. | ||
The file format is `dependency_name==dependency_version` or just a direct path to the dependency (for example to | ||
a github repo). For example: | ||
.. code:: | ||
pyls==0.1.2 | ||
colorama==1.2.2 | ||
git+https://github.com/tomv564/pyls-mypy.git | ||
:required: Yes | ||
""" | ||
|
||
server_filename = '' | ||
""" | ||
The file name of the binary used to start the server. | ||
:required: Yes | ||
""" | ||
|
||
@classmethod | ||
def manages_server(cls) -> bool: | ||
return True | ||
|
||
@classmethod | ||
def get_server(cls) -> Optional[ServerResourceInterface]: | ||
if not cls.__server: | ||
cls.__server = ServerPipResource(cls.storage_path(), cls.package_name, cls.requirements_txt_path, | ||
cls.server_filename) | ||
return cls.__server |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
from .helpers import run_command_sync | ||
from .server_resource_interface import ServerResourceInterface | ||
from .server_resource_interface import ServerStatus | ||
from LSP.plugin.core.typing import Dict, List, Optional, Tuple | ||
from sublime_lib import ResourcePath | ||
import os | ||
import shutil | ||
import sublime | ||
|
||
__all__ = ['ServerPipResource'] | ||
|
||
|
||
class ServerPipResource(ServerResourceInterface): | ||
""" | ||
An implementation of :class:`lsp_utils.ServerResourceInterface` implementing server management for | ||
pip-based servers. Handles installation and updates of the server in the package storage. | ||
:param storage_path: The path to the package storage (pass :meth:`lsp_utils.GenericClientHandler.storage_path()`) | ||
:param package_name: The package name (used as a directory name for storage) | ||
:param requirements_path: The path to the `requirements.txt` file, relative to the package directory. | ||
If the package `LSP-foo` has a `requirements.txt` file at the root then the path will be `requirements.txt`. | ||
:param server_binary_filename: The name of the file used to start the server. | ||
""" | ||
|
||
@classmethod | ||
def file_extension(cls) -> str: | ||
return '.exe' if sublime.platform() == 'windows' else '' | ||
|
||
@classmethod | ||
def python_exe(cls) -> str: | ||
return 'python' if sublime.platform() == 'windows' else 'python3' | ||
|
||
@classmethod | ||
def run(cls, *args, cwd: Optional[str] = None) -> str: | ||
output, error = run_command_sync(list(args), cwd=cwd) | ||
if error: | ||
raise Exception(error) | ||
return output | ||
|
||
def __init__(self, storage_path: str, package_name: str, requirements_path: str, | ||
server_binary_filename: str) -> None: | ||
self._storage_path = storage_path | ||
self._package_name = package_name | ||
self._requirements_path = 'Packages/{}/{}'.format(self._package_name, requirements_path) | ||
self._server_binary_filename = server_binary_filename | ||
self._status = ServerStatus.UNINITIALIZED | ||
|
||
def parse_requirements(self, requirements_path: str) -> List[Tuple[str, Optional[str]]]: | ||
requirements = [] # type: List[Tuple[str, Optional[str]]] | ||
lines = [line.strip() for line in ResourcePath(self._requirements_path).read_text().splitlines()] | ||
for line in lines: | ||
if line: | ||
parts = line.split('==') | ||
if len(parts) == 2: | ||
requirements.append(tuple(parts)) | ||
elif len(parts) == 1: | ||
requirements.append((parts[0], None)) | ||
return requirements | ||
|
||
def basedir(self) -> str: | ||
return os.path.join(self._storage_path, self._package_name) | ||
|
||
def bindir(self) -> str: | ||
bin_dir = 'Scripts' if sublime.platform() == 'windows' else 'bin' | ||
return os.path.join(self.basedir(), bin_dir) | ||
|
||
def server_exe(self) -> str: | ||
return os.path.join(self.bindir(), self._server_binary_filename + self.file_extension()) | ||
|
||
def pip_exe(self) -> str: | ||
return os.path.join(self.bindir(), 'pip' + self.file_extension()) | ||
|
||
def python_version(self) -> str: | ||
return os.path.join(self.basedir(), 'python_version') | ||
|
||
# --- ServerResourceInterface handlers ---------------------------------------------------------------------------- | ||
|
||
@property | ||
def binary_path(self) -> str: | ||
return self.server_exe() | ||
|
||
def needs_installation(self) -> bool: | ||
if os.path.exists(self.server_exe()) and os.path.exists(self.pip_exe()): | ||
if not os.path.exists(self.python_version()): | ||
return True | ||
with open(self.python_version(), 'r') as f: | ||
if f.readline().strip() != self.run(self.python_exe(), '--version').strip(): | ||
return True | ||
for line in self.run(self.pip_exe(), 'freeze').splitlines(): | ||
requirements = self.parse_requirements(self._requirements_path) | ||
for requirement, version in requirements: | ||
if not version: | ||
continue | ||
prefix = requirement + '==' | ||
if line.startswith(prefix): | ||
stored_version_str = line[len(prefix):].strip() | ||
if stored_version_str != version: | ||
return True | ||
self._status = ServerStatus.READY | ||
return False | ||
return True | ||
|
||
def install_or_update(self) -> None: | ||
shutil.rmtree(self.basedir(), ignore_errors=True) | ||
try: | ||
os.makedirs(self.basedir(), exist_ok=True) | ||
self.run(self.python_exe(), '-m', 'venv', self._package_name, cwd=self._storage_path) | ||
dest_requirements_txt_path = os.path.join(self._storage_path, self._package_name, 'requirements.txt') | ||
ResourcePath(self._requirements_path).copy(dest_requirements_txt_path) | ||
self.run(self.pip_exe(), 'install', '-r', dest_requirements_txt_path, '--disable-pip-version-check') | ||
with open(self.python_version(), 'w') as f: | ||
f.write(self.run(self.python_exe(), '--version')) | ||
except Exception: | ||
shutil.rmtree(self.basedir(), ignore_errors=True) | ||
raise | ||
self._status = ServerStatus.READY | ||
|
||
def get_status(self) -> int: | ||
return self._status |